JavaSE:多線程學習 01 初識進程 1.1 Process & Thread 1、首先簡要介紹程式。程式是指令和數據的有序集合,其本身沒有任何運行的含義,只是一個靜態的概念。 2、進程則是執行程式的一次執行過程,是一個動態的概念。是系統資源分配的單位。 3、通常在一個進程中可以包含若幹線程。線 ...
JavaSE:多線程學習
01 初識進程
1.1 Process & Thread
1、首先簡要介紹程式。程式是指令和數據的有序集合,其本身沒有任何運行的含義,只是一個靜態的概念。
2、進程則是執行程式的一次執行過程,是一個動態的概念。是系統資源分配的單位。
3、通常在一個進程中可以包含若幹線程。線程是CPU調度和執行的單位。
PS:很多線程是模擬出來的,真正的多線程是指有多個CPU,即多核,如伺服器。如果是模擬出來的多線程,即在只有一個CPU的情況下,在同一個時間點,CPU只能執行一條代碼。由於切換速度很快,所以會出現同時運行的錯覺。
4、線程:
- 線程就是獨立的執行路徑;
- 在程式運行時,哪怕沒有手動創建線程,後臺也會有多個線程,如主線程,gc線程(垃圾回收);
- main()稱之為主線程,為系統的入口,用於執行整個程式;
- 在一個進程中,如果開闢了多個線程,線程的運行由調度器(CPU)安排調度。調度器是與操作系統緊密相關的,先後順序是不能人為干預的;
- 對同一份資源進行操作時,會出現資源爭奪,需要加入併發控制。
- 線程會帶來額外的花銷,如CPU調度時間,併發控制消耗。
- 每個記憶體在自己的工作記憶體交互,記憶體控制不當會出現數據不一致。
02 創建線程
2.1 多線程有三種創建方式
1、Thread class(通過繼承Thread類)
2、Runnable介面(實現Runnable介面)
3、Callable介面(實現Callable介面)
2.2 Thread類
1、自定義線程類繼承Thread類
2、重寫run()方法,編寫線程執行體
3、在別的類中創建該線程單位,調用start()方法啟動多線程。
2.1-2.2小結
繼承Thread類
-
子類繼承Thread類具備多線程能力
-
啟動線程:thread類.start()
-
但不建議使用,避免OOP單繼承的特性
實現Runnable介面
- 實現介面Runnable具備多線程能力
- 啟動線程:傳入目標對象+thread類.start()
- 推薦使用:避免單繼承的局限性,具有很強的靈活性,方便同一個對象被多個線程使用
2.3 Callable介面
1、實現Callable,需要返回值類型
2、重寫Call方法,需要拋出異常
3、創建目標對象
4、創建執行服務
ExecutorService ser = Executors.newFixedThreadPool(線程數量)
5、提交執行
Future<Boolean> 線程名 = ser.submit(對象名);
6、獲取返回值
返回值類型 返回值名稱 = 線程名.get();
7、服務關閉
ser.shutdown();
2.4 靜態代理
1、真實對象和代理對象要實現同一介面
2、代理對象要代理真實角色
好處:
代理對象可以做很多真實角色做不了的事情
真實對象可以專註於自己的事
2.5 Lambda表達式
1、為什麼要使用Lambda表達式
- 避免匿名內部類定義過多
- 可以使得代碼更加簡潔
- 去掉大部分沒有意義的代碼,只留下最核心的內部邏輯
2、Lambda表達式的核心是採取函數式編程思想。因此,理解Function Interface(函數式介面)是學習Lambda表達式的關鍵。
3、函數式介面的定義:
- 任何一個介面,如果它只包含一個抽象方法,那它就是一個函數式介面。
public interface Runnable{
public abstract void run();
}
- 對於函數式介面,可以採取Lambda表達式的方法創建相關對象。
Lambda表達式使用註意:
- 只有無參Lambda表達式可以寫成如下模式:
new Thread(()->{語句});
- Lambda表達式只能在有一行代碼的情況下才能簡化成一行,如果有多行需要用代碼塊包裹
對象名 = 參數->{語句1;語句;}
- 使用lambda表達式一定要註意是針對函數式介面,即介面中只有一個抽象方法
- 如果lambda表達式中有多個參數,需要註意格式的統一,即如果有類型就必須都有類型,如果沒有就全都去掉
03 線程狀態
3.1 線程狀態簡介
!
3.1.1 線程停止
1、建議線程正常停止,限制次數,不要使用死迴圈
2、建議使用標誌位,即外部調整標誌位停止線程運行
3、不要使用stop或destroy等過時或者JDK不推薦的方法
3.1.2 線程休眠
-
sleep(時間)是指當前線程的阻塞秒數;
-
sleep存在異常InterruptedException;
-
sleep時間到達後線程進入就緒狀態
-
sleep可以模擬網路延時和倒計時等(巧妙運用sleep可以發現程式中併發多線程可能存在的問題)
-
每一個對象都有一個鎖,延時不會釋放鎖
3.1.3 線程禮讓
-
線程禮讓是指讓正在運行的線程暫停,但不阻塞
-
保存線程運行斷點,回到就緒狀態
-
讓CPU重新調度,不一定能禮讓成功。
3.1.4 線程合併
- Join方法合併線程,待此線程執行結束後,再執行其他線程,會導致其他線程阻塞
- 可以想象成插隊
3.2 線程狀態
3.2.1 線程狀態觀測
- 線程可以處於以下狀態之一:
- NEW
尚未啟動的線程處於此狀態。 - RUNNABLE
在Java虛擬機中執行的線程處於此狀態。 - BLOCKED
被阻塞等待監視器鎖定的線程處於此狀態。 - WAITING
正在等待另一個線程執行特定動作的線程處於此狀態。 - TIMED_WAITING
正在等待另一個線程執行動作達到指定等待時間的線程處於此狀態。 - TERMINATED
已退出的線程處於此狀態。
- NEW
3.2.2 線程優先順序
-
Java提供一個線程調度器來監控程式中啟動後進入就緒狀態的所有線程,線程調度器按照優先順序決定哪個線程優先執行
-
線程優先順序用數字來表示,範圍從1-10
-
Thread.MIN_PRIORITY = 1;
-
Thread.MAX_PRIORITY = 10;
-
Thread.NORM_PRIORITY = 5;
-
-
使用以下方法來改變或者獲取優先順序
-
getPriority() setPriority(inx XXX)
-
-
備註:1、優先順序的設定建議在start()之前
2、優先順序低只是意味著獲得調度的概率低,並不是優先順序低一定就會被後調用。歸根結底還是要看CPU的管理。
3.2.3 守護線程和用戶線程
- 線程分為用戶線程和守護線程
- JVM虛擬機必須確保用戶線程執行完畢
- 但是虛擬機不必等待守護線程執行結束
- 如:後臺監控記憶體,後臺記錄操作日誌,垃圾回收(gc線程)等
3.3 線程同步
3.3.1 線程同步簡介
1、併發:同一個對象被多個線程同時操作
2、處理多線程時,多個線程訪問同一個對象,並且某些線程還想修改這個對象,這時候就需要線程同步。線程同步實際上是一種等待機制,多個需要訪問此對象的線程進入這個對象的等待池形成隊列,等待前麵線程使用完畢,再調用下一個線程。
3、隊列和鎖:
由於同一進程的多個線程共用同一塊存儲空間,在帶來方便的同時,也帶來了訪問衝突的問題,為了保證數據在方法中被訪問的正確性,在訪問時加入鎖機制synchronized。當一個線程獲得對象的排它鎖時,就會獨占相關資源,其他線程必須等待該線程運行完畢釋放鎖。但由此會帶來一些問題:
- 一個線程持有鎖會導致其他所有需要此鎖的線程掛起。
- 在多線程競爭下,加鎖、釋放鎖會導致比較多的上下文切換和調度延時,從而引起性能降低。
- 如果一個優先順序高的線程等待一個優先順序低的線程釋放鎖,就會引起優先順序倒置,引發性能問題
3.3.2 三大不安全案例
3.3.2.1 系統購票
(詳見代碼)
3.3.2.2 銀行取錢
(詳見代碼)
3.3.2.3 線程安全性
(詳見代碼)
3.3.3 同步方法和同步塊
- 由前面可知,Java可以利用private關鍵字來保證數據對象只能被方法,因此仿照這個可以針對多線程併發提出一套機制,這套機制就是synchronized關鍵字。它包括兩種用法:synchronized方法和synchronized塊。
3.3.3.1 同步方法
-
同步方法
public synchronized void method(int args){}
-
synchronized方法控制對”對象“的訪問,每個對象對應一把鎖,每個synchronized方法都必須獲得調用該方法的對象的鎖才能執行,否則線程會阻塞。方法一旦執行,就獨占該鎖,直到該方法返回才釋放鎖,後面被阻塞的線程才能獲得這個鎖,繼續執行。
-
需要註意:若將一個大的方法申明為synchronized將會嚴重影響程式效率!
-
此外,synchronized方法針對的是方法中的this類屬性。
-
-
方法裡面需要修改的內容才需要鎖;鎖的數量太多會影響系統運行效率。
3.3.3.2 同步塊
-
同步塊
synchronized (obj){}
-
obj稱之為同步監視器
- Obj可以是任何對象,但是推薦使用共用資源作為同步監視器
-
同步方法中無需指定同步監視器,因為同步方法的同步監視器就是this,就是這個對象本身,或者是class。
-
同步監視器的執行過程:
-
第一個線程訪問,鎖定同步監視器,執行其中代碼。
-
第二個線程訪問,發現同步監視器被鎖定,無法訪問。
-
第一個線程訪問完畢,解鎖同步監視器。
-
第二個線程訪問,發現同步監視器沒有鎖,然後鎖定並訪問。
-
3.3.4 初識JUC
(詳見代碼)
3.4 死鎖和Lock鎖
-
死鎖
-
Lock鎖
-
從JDK5.0開始,Java提供了更加強大的線程同步機制——通過顯示定義同步鎖對象來實現同步。
-
同步鎖使用Lock對象充當java.until.concurrent.Lock介面,是控制多個線程對共用資源進行訪問的工具。鎖提供了對共用資源的獨占訪問,每次只能有一個線程對Lock對象加鎖。線程開始訪問共用資源之前應當先獲得Lock對象
-
ReentrantLock類實現了Lock,它擁有與synchronized相同的併發性和記憶體語義,在實現線程安全的控制中,比較常用的是ReentrantLock,可以顯示加鎖、釋放鎖。
-
class A{ private final ReentrantLock lock = new ReentrantLock(); public void m(){ lock.lock(); try{ //保證線程安全的代碼; }finally{ lock.unlock(); //如果同步代碼有異常,要將unlock()寫入finally語句塊 } } }
-
-
synchronized與Lock的對比
-
Lock是顯示鎖(手動開啟和關閉鎖,別忘記關閉鎖)synchronized是隱式鎖,出了作用域自動釋放(代碼塊或是方法)
-
Lock只有代碼塊鎖,synchronized有代碼塊和方法鎖
-
使用Lock鎖,JVM將花費更少的時間來調度線程,性能更好。並且具有更好的擴展性(提供更多的子類)
-
優先使用順序:
- Lock>同步代碼塊(已經進入了方法體,分配了相應資源)>同步方法(在方法體之外)
-
04 線程協作
4.1 線程通信問題
-
應用場景:生產者和消費者問題
-
假設倉庫中存有一定量的物品,生產者將生產出的產品放入倉庫,消費者消費倉庫中的產品;
-
如果倉庫中沒有物品,生產者將生產出的產品放入倉庫,消費者停止消費等待倉庫中有物品;
-
如果倉庫中物品已滿,生產者停止生產並等待,消費者消費倉庫中的產品。
-
-
這是一個線程同步問題,生產者和消費者共用同一個資源,並且生產者和消費者互為依賴,互為條件。
-
對於生產者,沒有生產產品前,要通知消費者等待,而生產了產品之後,又需要馬上通知消費者消費。
-
對於消費者,在消費之後,要通知生產者已經結束消費,需要生產新的產品以供消費。
-
在生產者消費者問題中,僅有synchronized是不夠的
-
synchronized可阻止併發更新同一個共用資源,實現了同步
-
synchronized不能用來實現不同線程之間的信息傳遞(通信)
-
-
-
Java提供了幾個方法解決線程之間的通信問題
方法名 作用 wait() 表示線程會一直等待,直到其他線程通知。與sleep不同,會釋放鎖 wait(long timeout) 指定等待的毫秒數 notify() 喚醒一個處於等待狀態的線程 notifyAll() 喚醒同一個對象上所有調用wait()方法的線程,優先順序別高的優先被調用 - 註意:這些均是Object類的方法,都只能在同步方法或者同步代碼塊中使用,否則會拋出異常IllegalMonitorStateException
4.2 解決辦法一:管程法
-
併發協作模型”生產者/消費者模式“--->管程法
-
生產者:負責生產數據的模塊(可能是方法,對象,線程,進程);
-
消費者:負責處理數據的模塊(可能是方法,對象,線程,進程);
-
緩衝區:消費者不能直接使用生產者的數據,他們之間有個“緩衝區”;
-
-
生產者將生產好的數據放入緩衝區,消費者從緩衝區拿出數據。
4.3 解決辦法二:信號燈法
- 信號燈法其實就是利用一個外部標誌位,結合wait()方法控制整個線程的運行
4.4 解決辦法三:線程池
-
JDK5.0開始提供了線程相關的API:ExcutorService和Executor
-
ExcutorService:真正的線程池介面。常見子類ThreadPoolExcutor
-
void execute(Runnable command):執行任務/命令,沒有返回值,一般用來執行Runnable
-
Future submit(Callable task):執行任務,有返回值,一般用來執行Callable -
void shutdown():關閉連接池
-
Executors:工具類、線程池的工廠類,用於創建並返回不同類型的線程池