1. 程式人生 > >多執行緒 之 Lock 鎖的實現原理

多執行緒 之 Lock 鎖的實現原理

1. Lock 的簡介及使用


Lock完全用Java寫成,在java這個層面是無關JVM實現的,Lock 介面主要有一下實現

//嘗試獲取鎖,獲取成功則返回,否則阻塞當前執行緒
void lock(); 

//嘗試獲取鎖,執行緒在成功獲取鎖之前被中斷,則放棄獲取鎖,丟擲異常 
void lockInterruptibly() throws InterruptedException; 

//嘗試獲取鎖,獲取鎖成功則返回true,否則返回false 
boolean tryLock(); 

//嘗試獲取鎖,若在規定時間內獲取到鎖,則返回true,否則返回false,未獲取鎖之前被中斷,則丟擲異常 
boolean tryLock(long time, TimeUnit unit)
                                   throws InterruptedException; 

//釋放鎖
void unlock(); 

//返回當前鎖的條件變數,通過條件變數可以實現類似notify和wait的功能,一個鎖可以有多個條件變數
Condition newCondition();

2. 實現Lock介面的基本思想(AQS、CAS)

AQS :AbstractQueuedSynchronized 核心概念:一個是表示(鎖)狀態的變數、一個是佇列 C
AS: 簡單的來說,CAS有3個運算元,記憶體值V,舊的預期值A,要修改的新值B。當且僅當預期值A和記憶體值V相同時,將記憶體值V修改為B,否則返回V。這是一種樂觀鎖的思路,它相信在它修改之前,沒有其它執行緒去修改它;而Synchronized是一種悲觀鎖,它認為在它修改之前,一定會有其它執行緒去修改它,悲觀鎖效率很低。缺點:會產生ABA問題,即A被修改成B,然後又被修改成A,不能感知到修改

3.ReentrantLock具體實現


在java.util.concurrent.locks包中有很多Lock的實現類,常用的有ReentrantLock、ReadWriteLock(實現類ReentrantReadWriteLock),其實現都依賴java.util.concurrent.

AbstractQueuedSynchronizer類,實現思路都大同小異,因此我們。以ReentrantLock作為講解切入點。

ReentrantLock的Lock過程

ReentrantLock把所有Lock介面的操作都委派到一個Sync類上,該類繼承了AbstractQueuedSynchronizer(簡稱AQS)

1、在初始化ReentrantLock的時候,如果我們不傳引數是否公平,那麼預設使用非公平鎖,也就是NonfairSync。

2、當我們呼叫ReentrantLock的lock方法的時候,實際上是呼叫了NonfairSync的lock方法,這個方法先用CAS操作,去嘗試搶佔該鎖。

如果成功,就把當前執行緒設定在這個鎖上,表示搶佔成功。如果失敗,則呼叫acquire模板方法,等待搶佔。

3、呼叫acquire(1)實際上使用的是AbstractQueuedSynchronizer的acquire方法,它是一套鎖搶佔的模板,總體原理是先去嘗試獲取鎖,

如果沒有獲取成功,就在CLH佇列中增加一個當前執行緒的節點,表示等待搶佔。然後進入CLH佇列的搶佔模式,進入的時候也會去執行一

次獲取鎖的操作,如果還是獲取不到,就呼叫LockSupport.park將當前執行緒掛起。當持有鎖的那個執行緒呼叫unlock的時候,會將CLH

佇列的頭節點的下一個節點上的執行緒喚醒,呼叫的是LockSupport.unpark方法。

ReentrantLock的UnLock過程

1、呼叫unlock方法,其實是直接呼叫AbstractQueuedSynchronizer的release操作。

2、進入release方法,內部先嚐試tryRelease操作,主要是去除鎖的獨佔執行緒,然後將狀態減一,這裡減一主要是考慮到可重入鎖可能自身會

多次佔用鎖,只有當狀態變成0,才表示完全釋放了鎖。

3、一旦tryRelease成功,則將CHL佇列的頭節點的狀態設定為0,然後喚醒下一個非取消的節點執行緒。

4、一旦下一個節點的執行緒被喚醒,被喚醒的執行緒就會進入acquireQueued程式碼流程中,去獲取鎖。

執行緒使用ReentrantLock獲取鎖分為兩個階段,第一個階段是初次競爭,第二個階段是基於CHL佇列的競爭。在初次競爭的時候是否考慮佇列節點直接區分出了公平鎖和非公平鎖。在基於CHL佇列的鎖競爭中,依靠CAS操作保證原子操作,依靠LockSupport來做執行緒的掛起和喚醒,使用佇列來保證併發執行變成了序列執行,從而消除了併發所帶來的問題。總體來說,ReentrantLock是一個比較輕量級的鎖,而且使用面向物件的思想去實現了鎖的功能,比原來的synchronized關鍵字更加好理解


4. Lock 與Synchronized的區別

1、synchronized內建關鍵字,在JVM層面實現,發生異常時,會自動釋放執行緒佔有的鎖,因此不會導致死鎖現象發生。Lock在發生異常時,如果沒有主動通過unLock()去釋放鎖,則很可能造成死鎖現象,因此使用Lock時需要在finally塊中釋放鎖;
2、Lock具有高階特性:時間鎖等候、可中斷鎖等候、無塊結構鎖、多個條件變數或者鎖投票,可以知道有沒有成功獲取鎖 3、當競爭資源非常激烈時(即有大量執行緒同時競爭),此時Lock的效能要遠遠優於synchronized 結論:建議用 synchronized 開發,直到確實證明 synchronized 不合適,而不要僅僅是假設如果使用 ReentrantLock “效能會更好”。

在JDK1.5中,synchronized是效能低效的。因為這是一個重量級操作,它對效能最大的影響是阻塞的是實現,掛起執行緒和恢復執行緒的操作都需要轉入核心態中完成,這些操作給系統的併發性帶來了很大的壓力。相比之下使用Java提供的Lock物件,效能更高一些。Brian Goetz對這兩種鎖在JDK1.5、單核處理器及雙Xeon處理器環境下做了一組吞吐量對比的實驗,發現多執行緒環境下,synchronized的吞吐量下降的非常嚴重,而ReentrankLock則能基本保持在同一個比較穩定的水平上。但與其說ReetrantLock效能好,倒不如說synchronized還有非常大的優化餘地,於是到了JDK1.6,發生了變化,對synchronize加入了很多優化措施,有自適應自旋,鎖消除,鎖粗化,輕量級鎖,偏向鎖等等。導致在JDK1.6上synchronize的效能並不比Lock差。官方也表示,他們也更支援synchronize,在未來的版本中還有優化餘地,所以還是提倡在synchronized能實現需求的情況下,優先考慮使用synchronized來進行同步。