Java多執行緒學習(3)執行緒同步與執行緒通訊

快樂的博格巴發表於2018-10-15

當多個執行緒訪問同一個資料時會出現執行緒安全問題。

1 同步程式碼塊

synchronized(obj){
    
}
複製程式碼

obj是同步監視器,執行緒開始執行同步程式碼塊之前,必須先獲得對同步監視器的鎖定。

2 同步方法

對於用synchronized修飾的例項方法(非static方法)而言,無需顯示指定同步監視器,同步方法的同步監視器是this,也就是呼叫該引數的物件。

同步程式碼塊和同步方法要符合“加鎖——修改——釋放鎖”的邏輯:在任何執行緒修改指定資源之前,首先對該資源加鎖,加鎖期間其他執行緒無法修改該資源,當該執行緒修改完成後,該執行緒釋放對該執行緒的鎖定。

以下幾種情況,當前執行緒會釋放同步監視器:
1 同步程式碼塊執行結束;
2 同步程式碼塊中遇到了break,return終止了該程式碼塊;
3 在同步程式碼塊中出現了未處理的Error或Exception;
4 當前執行緒執行同步程式碼塊或同步方法時,程式執行了同步監視器物件的wait方法,則當前執行緒暫停,並釋放同步監視器。

注:sleep(),yield(),suspend()不會釋放同步監視器。

3 同步鎖 Lock

從Java5開始,可以通過顯示定義同步鎖物件來實現同步,同步鎖由Lock物件充當。
public interface Lock
Lock 實現提供了比使用 synchronized 方法和語句可獲得的更廣泛的鎖定操作。此實現允許更靈活的結構,可以具有差別很大的屬性,可以支援多個相關的 Condition 物件。

鎖是控制多個執行緒對共享資源進行訪問的工具。通常,鎖提供了對共享資源的獨佔訪問。一次只能有一個執行緒獲得鎖,對共享資源的所有訪問都需要首先獲得鎖。不過,某些鎖可能允許對共享資源併發訪問,如 ReadWriteLock 的讀取鎖。
比較常用的是ReentrantLock。

private final ReentrantLock lock = new ReentrantLock();
...
lock.lock();
...
lock.unlock();
複製程式碼

如果獲取了多個鎖,必須以相反的順序釋放;

4 死鎖

死鎖是這樣一種情形:多個執行緒同時被阻塞,它們中的一個或者全部都在等待某個資源被釋放。由於執行緒被無限期地阻塞,因此程式不可能正常終止。

java 死鎖產生的四個必要條件:

1、互斥使用,即當資源被一個執行緒使用(佔有)時,別的執行緒不能使用 2、不可搶佔,資源請求者不能強制從資源佔有者手中奪取資源,資源只能由資源佔有者主動釋放。 3、請求和保持,即當資源請求者在請求其他的資源的同時保持對原有資源的佔有。 4、迴圈等待,即存在一個等待佇列:P1佔有P2的資源,P2佔有P3的資源,P3佔有P1的資源。這樣就形成了一個等待環路。 當上述四個條件都成立的時候,便形成死鎖。當然,死鎖的情況下如果打破上述任何一個條件,便可讓死鎖消失。

執行緒通訊:

1 傳統執行緒通訊

wait():導致當前執行緒等待,知道其他執行緒呼叫該同步監視器的notify()或notifyAll()方法來喚醒該執行緒。
notify():隨機喚醒此同步監視器上的一個執行緒;
notifyAll():喚醒此同步監視器上的所有執行緒。

2 使用Condition控制執行緒通訊

如果程式使用的是Lock物件同步,則不存在隱式監視器,也就不能使用wait(),notify(),notifyAll()來進行執行緒同步了。
當使用Lock物件來保證同步是,java提供了一個Condition類來保持協調,使用Condition可以讓那些已經得到Lock物件卻無法執行的執行緒釋放Lock物件,Condition物件也可以喚醒其他處於等待的執行緒。
Condition例項被繫結在一個Lock物件上,要獲得特定的Lock例項的Condition例項。呼叫Lock物件的newCondition()方法即可。Condition類提供瞭如下三個方法:
1 wait():導致當前執行緒等待;wait()方法有很多變體;
2 signal():喚醒在此Lock物件上等待的單個執行緒,,只有當前執行緒放棄對該Lock物件的鎖定後,才可以執行被喚醒的執行緒;
3 signalAll():喚醒在此Lock物件上等待的所有執行緒,只有當前執行緒放棄對該Lock物件的鎖定後,才可以執行被喚醒的執行緒。

3 使用阻塞佇列(BlockingQueue)控制執行緒通訊

BlockingQueue也是Queue的子介面,但是它的作用不是作為容器,而是作為執行緒同步的工具。
當生產者執行緒試圖向BlockingQueue中放入元素時,如果佇列已滿,則執行緒被阻塞;
當消費者執行緒試圖從BlockingQueue中取出元素時,如果該佇列已空,則該執行緒被阻塞。
BlockingQueue提供如下兩個支撐阻塞的方法。
put(E e):嘗試把E元素放入BlockingQueue中,如果佇列已滿,則執行緒被阻塞;
trake():嘗試從BlockingQueue的頭部取出元素,,如果該佇列已空,則該執行緒被阻塞。

BlockingQueue包含如下5個實現類:
1 ArrayBlockingQueue; 2 LinkedBlockingQueue; 3 PriorityBlockingQueue,取元素時,並不是取出在佇列中存在時間最長的數,而是佇列中最小的元素。
4 SynchronounsQueue:同步佇列。對該佇列的存取必須交替進行。
5 DelayQueue

4 執行緒組和未處理的異常

執行緒組ThreadGroup,可以對一批執行緒進行分類管理,java允許程式直接對執行緒組進行控制。對執行緒組的控制相當於同時控制這批執行緒,使用者建立的所有執行緒都屬於指定執行緒組,如果程式沒有指定執行緒屬於哪個執行緒組,則該執行緒屬於預設執行緒組。
Thread(ThreadGruop, Runnable target);
Thread(ThreadGruop, Runnable target, String name);
Thread(ThreadGruop, String name);

ThreadGruop(String name); ThreadGruop(ThreadGruop target, String name);

ThreadGroup類提供瞭如下幾個常用方法來操作整個執行緒組裡的所有執行緒。
int activeCount():返回此執行緒組中活動執行緒的數目;
interrupt(): 中斷此執行緒組中的所有執行緒;
isDaemon():判斷該執行緒組是否是後臺執行緒組;
setDaemon():把該執行緒組設定成後臺執行緒組;
setMaxPriority(int pri):設定執行緒組的最高優先順序。

5 執行緒池

系統啟動一個執行緒的成本是比較高的,因為它涉及與作業系統的互動,使用執行緒池可以很好地提高效能,尤其是當程式中需要建立大量生命週期很短的執行緒時。

java5增加了Executers工廠類來產生執行緒池。

使用執行緒池來執行執行緒任務的步驟:
1 呼叫Executors類的靜態方法建立一個ExecutorService物件,該物件代表一個執行緒池。
2 建立Runnable實現類或Callable實現類的例項,作為執行緒執行任務。
3 呼叫ExecutorService物件的submit物件來提交Runnable例項或Callable例項。
4 當不想提交任何執行緒時,使用shutdown()方法來關閉執行緒池。

相關文章