JUC簡介
在Java 5.0 提供了java.util.concurrent(簡稱JUC )包,在此包中增加了在併發程式設計中很常用的實用工具類,用於定義類似於執行緒的自定義子系統,包括執行緒池、非同步IO 和輕量級任務框架。提供可調的、靈活的執行緒池。還提供了設計用於多執行緒上下文中的Collection 實現等。
-
記憶體可見性問題: 當多個執行緒操作共享資料時,彼此不可見
-
volatile 關鍵字: 當多個執行緒進行操作共享資料時,可以保證記憶體中的資料可見。 相較於 synchronized 是一種較為輕量級的同步策略。
注意: 1. volatile 不具備“互斥性” 2. volatile 不能保證變數的“原子性”
-
i++ 的原子性問題:i++ 的操作實際上分為三個步驟“讀-改-寫”
int i = 10; i = i++; //10 int temp = i; i = i + 1; i = temp; 複製程式碼
-
原子變數:在 java.util.concurrent.atomic 包下提供了一些原子變數。
- volatile 保證記憶體可見性
- CAS(Compare-And-Swap) 演算法保證資料變數的原子性 CAS 演算法是硬體對於併發操作的支援 CAS 包含了三個運算元: ①記憶體值 V ②預估值 A ③更新值 B 當且僅當 V == A 時, V = B; 否則,不會執行任何操作。
- CAS演算法要比同步鎖的效率高很多,因為執行緒不會阻塞,它可以馬上去讀,然後更新值。
- CAS也是硬體對於併發操作的支援
-
CopyOnWriteArrayList/CopyOnWriteArraySet : “寫入並複製” 注意:新增操作多時,效率低,因為每次新增時都會進行復制,開銷非常的大。併發迭代操作多時可以選擇。
-
java建立執行緒的4中方式
- 繼承Thread類 (1)定義Thread類的子類,並重寫該類的run方法,該run方法的方法體就代表了執行緒要完成的任務。因此把run()方法稱為執行體。 (2)建立Thread子類的例項,即建立了執行緒物件。 (3)呼叫執行緒物件的start()方法來啟動該執行緒。
- 通過Runnable介面建立執行緒類 (1)定義runnable介面的實現類,並重寫該介面的run()方法,該run()方法的方法體同樣是該執行緒的執行緒執行體。 (2)建立 Runnable實現類的例項,並依此例項作為Thread的target來建立Thread物件,該Thread物件才是真正的執行緒物件。 (3)呼叫執行緒物件的start()方法來啟動該執行緒。
- 通過Callable和Future建立執行緒 (1)建立Callable介面的實現類,並實現call()方法,該call()方法將作為執行緒執行體,並且有返回值。 (2)建立Callable實現類的例項,使用FutureTask類來包裝Callable物件,該FutureTask物件封裝了該Callable物件的call()方法的返回值。 (3)使用FutureTask物件作為Thread物件的target建立並啟動新執行緒。 (4)呼叫FutureTask物件的get()方法來獲得子執行緒執行結束後的返回值 (5)FutureTask也可以用於閉鎖的操作。
-
執行緒池方式
- 對比:
- 採用實現Runnable、Callable介面的方式創見多執行緒時。
- 優勢是:
- 執行緒類只是實現了Runnable介面或Callable介面,還可以繼承其他類。
- 在這種方式下,多個執行緒可以共享同一個target物件,所以非常適合多個相同執行緒來處理同一份資源的情況,從而可以將CPU、程式碼和資料分開,形成清晰的模型,較好地體現了物件導向的思想。
- 劣勢是: 程式設計稍微複雜,如果要訪問當前執行緒,則必須使用Thread.currentThread()方法。
- 優勢是:
- 使用繼承Thread類的方式建立多執行緒時。
- 優勢: 編寫簡單,如果需要訪問當前執行緒,則無需使用Thread.currentThread()方法,直接使用this即可獲得當前執行緒。
- 劣勢: 執行緒類已經繼承了Thread類,所以不能再繼承其他父類。
- 採用實現Runnable、Callable介面的方式創見多執行緒時。
- 對比:
-
用於解決多執行緒執行緒安全的方式:
- jdk1.5以前:
- Synchronize:隱式鎖
- 同步程式碼塊
- 同步方法
- Synchronize:隱式鎖
- jdk1.5以後:
- 同步鎖lock:
- 顯示鎖,必須通過lock()方法進行上鎖,同時也必須通過unLock()方法釋放鎖
- 問題:需要保證鎖會釋放,所以一般unLock()要放在finally下面
- 同步鎖lock:
- jdk1.5以前:
-
生產者與消費者
- 不用等待喚醒機制,會產生的問題:
-
重複呼叫佔用資源問題
- 原因分析 : 上述的情況是當沒貨的時候還會繼續呼叫該方法,從而佔用資源,二貨滿的情況下也會重複呼叫進貨方法,佔用資源,這樣是不合理的。
- 解決方式: 當貨滿了,應該停止進貨,釋放鎖讓消費者消費,當沒貨了應該停止消費釋放鎖,讓進貨,這是我們想要的邏輯。使用wait()和notifyAll()這兩個方法來實現。
-
執行緒阻塞無法喚醒
- 原因分析 當product比較小假如是1的時候,有可能生產者先迴圈結束, 消費者還沒結束,一直在waite無法得到喚醒就一直等待 程式就會停在那裡
- 解決方式 去掉else,保證每次都會喚醒另外一個執行緒
-
虛假喚醒問題 當只有一個Factory有兩個Consumer的時候就會出現虛假喚醒問題。導致商品都成了負數了。
- 原因分析: 當建立對個生產消費者執行緒的時候,會產生虛假喚醒,導致product 為負數,是因為當消費者執行緒A發現沒貨的時候,wait之後釋放鎖, 另外一個消費者執行緒B獲得鎖開始執行,結果也沒貨,開始wait,當生產者生產之後notifyAll,A,B執行緒開始繼續向下執行,結果進行了兩次–操作,導致product成為了負數
- 解決方式: JDK文件object的wait方法已經考慮到這種情況,防止虛假喚醒,應該放在迴圈中,多次進行檢查,直到滿足條件才進行下一步。即不要使用if來進行判斷而用while迴圈來進行判斷
-
守護執行緒解決執行緒阻塞 上面解決了虛假喚醒問題,但是當多個消費者和一個生產者的時候,生產者有可能先結束迴圈,但是消費者還沒結束,結果到了其他消費者的時候發現product是小於0的於是就wait,程式一直等待得不到結束,就會一直在wait()
-
解決方式: 在共享資源clerk類中定義生產者執行緒標誌位,在main執行緒中建立一個執行緒設定為守護執行緒並啟動,在該守護執行緒中建立匿名內部類Runnable並在run方法中判斷生產者執行緒isAlive()如果生產者執行緒結束,就把標誌位置為false,該標識位和消費者執行緒的while判斷條件中串聯。當生產者執行緒為false的之後短路,使得消費和執行緒啥都不做,直到執行緒結束。
- Clerk中設定Factory執行緒的標誌位
private boolean facctoryFlg = true;//工廠執行緒結束的標誌位,為false表示執行緒執行完畢 public boolean isFacctoryFlg() { return facctoryFlg; } public void setFacctoryFlg(boolean facctoryFlg) { this.facctoryFlg = facctoryFlg; } 複製程式碼
- 主方法中建立守護執行緒
//建立守護執行緒 Thread daemon = new Thread(new Runnable() { @Override public void run() { while(true){ if(!tf.isAlive()){ clerk.setFacctoryFlg(false); System.out.println("factory--------------"+tf.isAlive()); break; } } } }); daemon.setDaemon(true);//設定為守護執行緒(後臺執行緒) daemon.start(); 複製程式碼
- 修改Clerk的sale方法:
//售貨 public synchronized void sale(){ while(product<=0){ //當Factory執行緒結束的時候,直接結束sale方法 if(!isFacctoryFlg()){ return; } System.out.println("沒貨了"); try { this.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println(Thread.currentThread().getName()+"賣貨"+product); --product; notifyAll(); } ``` 複製程式碼
-
-
- 不用等待喚醒機制,會產生的問題: