![]() |
VOOZH | about |
在java中有兩種方法實現鎖機制,一種是在前一篇博客中(Java並發編程實戰(2):synchronized)介紹的synchronized,而另一種是比synchronized更加強大和靈活的Lock。Lock確保當一個線程位於代碼的臨界區時,另一個線程不進入臨界區,相對於synchronized,Lock接口及其實現類提供了更加強大、靈活的鎖機制。
一個簡單的鎖在使用synchronized時,我們是這樣使用鎖的:
synchronized可以確保在同一時間內只有一個線程在執行dosomething。下面是使用lock替代synchronized:
lock方法會對Lock實例對象進行加鎖,因此所有對該對象調用lock方法的線程都會被阻塞,直到該Lock對象的unlock方法被調用。
【以下引自:Java中的鎖】
當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不會保證哪一個線程會獲得喚醒,任何線程都有可能處於繼續等待的狀態。因此存在這樣一個風險:一個等待線程從來得不到喚醒,因為其他等待線程總是能被獲得喚醒。
為了解決線程「飢餓」的問題,我們可以使用鎖實現公平性。
鎖的可重入性我們知道當線程請求一個由其它線程持有鎖的對象時,該線程會阻塞,但是當線程請求由自己持有鎖的對象時,是否可以成功呢?答案是可以成功的,成功的保障就是線程鎖的「可重入性」。
「可重入」意味著自己可以再次獲得自己的內部鎖,而不需要阻塞。如下:
如果所是不可重入的,上面的代碼就會死鎖,因為調用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並發性和多線程】飢餓和公平