標籤:靈活 void 重要 body ext 運行時 condition 並發 unlock
在java中有兩種方法實現鎖機制,一種是在前一篇部落格中(【java7並發編程實戰】-----線程同步機制:synchronized)介紹的synchronized,而另一種是比synchronized更加強大和領過的Lock。Lock確保當一個線程位於代碼的臨界區時,另一個線程不進入臨界區,相對於synchronized,Lock介面及其實作類別提供了更加強大、靈活的鎖機制。
一個簡單的鎖
在使用synchronized時,我們是這樣使用鎖的:
public class ThreadTest { public void test(){ synchronized(this){ //do something } }}
synchronized可以確保在同一時間內只有一個線程在執行dosomething。下面是使用lock替代synchronized:
public class ThreadTest { Lock lock = new Lock(); public void test(){ lock.lock(); //do something lock.unlock(); }}
lock()方法會對Lock執行個體對象進行加鎖,因此所有對該對象調用lock()方法的線程都會被阻塞,直到該Lock對象的unlock()方法被調用。
【以下引自:Java中的鎖】
public class Lock{ private boolean isLocked = false; public synchronized void lock() throws InterruptedException{ while(isLocked){ wait(); } isLocked = true; } public synchronized void unlock(){ isLocked = false; notify(); }}
當isLocked為true時,調用lock()的線程在wait()調用上阻塞等待。為防止該線程沒有收到notify()調用也從wait()中返回,這個線程會重新去檢查isLocked條件以決定當前是否可以安全地繼續執行還是需要重新保持等待,而不是認為線程被喚醒了就可以安全地繼續執行了。如果isLocked為false,當前線程會退出while(isLocked)迴圈,並將isLocked設回true,讓其它正在調用lock()方法的線程能夠在Lock執行個體上加鎖。
鎖的公平性
公平性的對立面是饑餓。那麼什麼是“饑餓”呢?如果一個線程因為其他線程在一直搶佔著CPU而得不到CPU已耗用時間,那麼我們就稱該線程被“饑餓致死”。而解決饑餓的方案則被稱之為“公平性”——所有線程均可以公平地獲得CPU運行機會。
導致線程饑餓主要有如下幾個原因:
高優先順序線程吞噬所有的低優先順序線程的CPU時間。我們可以為每個線程單獨設定其優先順序,從1到10。優先順序越高的線程獲得CPU的時間越多。對大多數應用來說,我們最好是不要改變其優先順序值。
線程被永久堵塞在一個等待進入同步塊的狀態。java的同步代碼區是導致線程饑餓的重要因素。java的同步代碼塊並不會保證進入它的線程的先後順序。這就意味著理論上存在一個或者多個線程在試圖進入同步代碼區時永遠被堵塞著,因為其他線程總是不斷優於他獲得訪問權,導致它一直得到不到CPU運行機會被“饑餓致死”。
線程在等待一個本身也處於永久等待完成的對象。如果多個線程處在wait()方法執行上,而對其調用notify()不會保證哪一個線程會獲得喚醒,任何線程都有可能處於繼續等待的狀態。因此存在這樣一個風險:一個等待線程從來得不到喚醒,因為其他等待線程總是能被獲得喚醒。
為瞭解決線程“饑餓”的問題,我們可以使用鎖實現公平性。
鎖的可重新進入性
我們知道當線程請求一個由其它線程持有鎖的對象時,該線程會阻塞,但是當線程請求由自己持有鎖的對象時,是否可以成功呢?答案是可以成功的,成功的保障就是線程鎖的“可重新進入性”。
“可重新進入”意味著自己可以再次獲得自己的內部鎖,而不需要阻塞。如下:
public class Father { public synchronized void method(){ //do something }}public class Child extends Father{ public synchronized void method(){ //do something super.method(); }}
如果所是不可重新進入的,上面的代碼就會死結,因為調用child的method(),首先會擷取父類Father的內建鎖然後擷取Child的內建鎖,當調用父類的方法時,需要再次後去父類的內建鎖,如果不可重新進入,可能會陷入死結。
java多線程的可重新進入性的實現是通過每個鎖關聯一個請求計算和一個佔有它的線程,當計數為0時,認為該鎖是沒有被佔有的,那麼任何線程都可以獲得該鎖的佔有權。當某一個線程請求成功後,JVM會記錄該鎖的持有線程 並且將計數設定為1,如果這時其他線程請求該鎖時則必須等待。當該線程再次請求請求獲得鎖時,計數會+1;當佔有線程退出同步代碼塊時,計數就會-1,直到為0時,釋放該鎖。這時其他線程才有機會獲得該鎖的佔有權。
lock及其實作類別
java.util.concurrent.locks提供了非常靈活鎖機制,為鎖定和等待條件提供一個架構的介面和類,它不同於內建同步和監視器,該架構允許更靈活地使用鎖定和條件。它的類結構圖如下:
ReentrantLock:一個可重新進入的互斥鎖,為lock介面的主要實現。
ReentrantReadWriteLock:
ReadWriteLock:ReadWriteLock 維護了一對相關的鎖,一個用於唯讀操作,另一個用於寫入操作。
Semaphore:一個計數訊號量。
Condition:鎖的關聯條件,目的是允許線程擷取鎖並且查看等待的某一個條件是否滿足。
CyclicBarrier:一個同步輔助類,它允許一組線程互相等待,直到到達某個公用屏障點。
後面將會對這些類進行詳細說明。
參考資料:
1、Java中的鎖
2、【Java並發性和多線程】饑餓和公平
Java - “JUC”鎖