同步中的四種鎖synchronized、ReentrantLock、ReentrantReadWriteLock、StampedLock

爲了更好的支持併發程序,JDK內部提供了多種鎖。本文總結4種鎖。html

1.synchronized同步鎖

使用併發

synchronized本質上就2種鎖:高併發

1.鎖同步代碼塊性能

2.鎖方法ui

可用object.wait() object.notify()來操做線程等待喚醒spa

原理:synchronized細節的描述傳送門:jdk源碼剖析三:鎖Synchronized 線程

性能和建議:JDK6以後,在併發量不是特別大的狀況下,性能中等且穩定。建議新手使用。設計

2.ReentrantLock重入鎖(Lock接口)

使用:ReentrantLock是Lock接口的實現類。Lock接口的核心方法是lock(),unlock(),tryLock()。可用Condition來操做線程:code

如上圖,await()和object.wait()相似,singal()和object.notify()相似,singalAll()和object.notifyAll()相似orm

原理核心類AbstractQueuedSynchronizer,經過構造一個基於阻塞的CLH隊列容納全部的阻塞線程,而對該隊列的操做均經過Lock-Free(CAS)操做,但對已經得到鎖的線程而言,ReentrantLock實現了偏向鎖的功能。

性能和建議:性能中等,建議須要手動操做線程時使用。

 

3.ReentrantReadWriteLock可重入讀寫鎖(ReadWriteLock接口)

使用:ReentrantReadWriteLock是ReadWriteLock接口的實現類。ReadWriteLock接口的核心方法是readLock(),writeLock()。實現了併發讀、互斥寫。但讀鎖會阻塞寫鎖,是悲觀鎖的策略。

原理類圖以下:

JDK1.8下,如圖ReentrantReadWriteLock有5個靜態方法:

  • Sync:繼承於經典的AbstractQueuedSynchronizer(傳說中的AQS),是一個抽象類,包含2個抽象方法readerShouldBlock();writerShouldBlock()
  • FairSync和NonfairSync:繼承於Sync,分別實現了公平/非公平鎖。
  • ReadLock和WriteLock:都是Lock實現類,分別實現了讀、寫鎖。ReadLock是共享的,而WriteLock是獨佔的。因而Sync類覆蓋了AQS中獨佔和共享模式的抽象方法(tryAcquire/tryAcquireShared等),用同一個等待隊列來維護讀/寫排隊線程,而用一個32位int state標示和記錄讀/寫鎖重入次數--Doug Lea把狀態的高16位用做讀鎖,記錄全部讀鎖重入次數之和,低16位用做寫鎖,記錄寫鎖重入次數。因此不管是讀鎖仍是寫鎖最多隻能被持有65535次。

性能和建議:適用於讀多寫少的狀況。性能較高。

  • 公平性
  1. 非公平鎖(默認),爲了防止寫線程餓死,規則是:當等待隊列頭部結點是獨佔模式(即要獲取寫鎖的線程)時,只有獲取獨佔鎖線程能夠搶佔,而試圖獲取共享鎖的線程必須進入隊列阻塞;當隊列頭部結點是共享模式(即要獲取讀鎖的線程)時,試圖獲取獨佔和共享鎖的線程均可以搶佔。
  2. 公平鎖,利用AQS的等待隊列,線程按照FIFO的順序獲取鎖,所以不存在寫線程一直等待的問題。
  • 重入性:讀寫鎖均是可重入的,讀/寫鎖重入次數保存在了32位int state的高/低16位中。而單個讀線程的重入次數,則記錄在ThreadLocalHoldCounter類型的readHolds裏。
  • 鎖降級:寫線程獲取寫入鎖後能夠獲取讀取鎖,而後釋放寫入鎖,這樣就從寫入鎖變成了讀取鎖,從而實現鎖降級。
  • 鎖獲取中斷:讀取鎖和寫入鎖都支持獲取鎖期間被中斷。
  • 條件變量:寫鎖提供了條件變量(Condition)的支持,這個和獨佔鎖ReentrantLock一致,可是讀鎖卻不容許,調用readLock().newCondition()會拋出UnsupportedOperationException異常。

應用:

4.StampedLock戳鎖

使用

StampedLock控制鎖有三種模式(排它寫,悲觀讀,樂觀讀),一個StampedLock狀態是由版本和模式兩個部分組成,鎖獲取方法返回一個數字做爲票據stamp,它用相應的鎖狀態表示並控制訪問。下面是JDK1.8源碼自帶的示例:

 1 public class StampedLockDemo {
 2     //一個點的x,y座標
 3     private double x,y;
 4     private final StampedLock sl = new StampedLock();
 5 
 6     //【寫鎖(排它鎖)】
 7     void move(double deltaX,double deltaY) {// an exclusively locked method 
 8         /**stampedLock調用writeLock和unlockWrite時候都會致使stampedLock的stamp值的變化
 9          * 即每次+1,直到加到最大值,而後從0從新開始 
10          **/
11         long stamp =sl.writeLock(); //寫鎖
12         try {
13             x +=deltaX;
14             y +=deltaY;
15         } finally {
16             sl.unlockWrite(stamp);//釋放寫鎖
17         }
18     }
19 
20     //【樂觀讀鎖】
21     double distanceFromOrigin() { // A read-only method
22         /**
23          * tryOptimisticRead是一個樂觀的讀,使用這種鎖的讀不阻塞寫
24          * 每次讀的時候獲得一個當前的stamp值(相似時間戳的做用)
25          */
26         long stamp = sl.tryOptimisticRead();
27         //這裏就是讀操做,讀取x和y,由於讀取x時,y可能被寫了新的值,因此下面須要判斷
28         double currentX = x, currentY = y;
29         /**若是讀取的時候發生了寫,則stampedLock的stamp屬性值會變化,此時須要重讀,
30          * 再重讀的時候須要加讀鎖(而且重讀時使用的應當是悲觀的讀鎖,即阻塞寫的讀鎖)
31          * 固然重讀的時候還可使用tryOptimisticRead,此時須要結合循環了,即相似CAS方式
32          * 讀鎖又從新返回一個stampe值*/
33         if (!sl.validate(stamp)) {//若是驗證失敗(讀以前已發生寫)
34             stamp = sl.readLock(); //悲觀讀鎖
35             try {
36                 currentX = x;
37                 currentY = y;
38             }finally{
39                 sl.unlockRead(stamp);//釋放讀鎖
40             }
41         }
42         //讀鎖驗證成功後執行計算,即讀的時候沒有發生寫
43         return Math.sqrt(currentX *currentX + currentY *currentY);
44     }
45 
46     //【悲觀讀鎖】
47     void moveIfAtOrigin(double newX, double newY) { // upgrade
48         // 讀鎖(這裏可用樂觀鎖替代)
49         long stamp = sl.readLock();
50         try {
51             //循環,檢查當前狀態是否符合
52             while (x == 0.0 && y == 0.0) {
53             /**
54              * 轉換當前讀戳爲寫戳,即上寫鎖
55              * 1.寫鎖戳,直接返回寫鎖戳
56              * 2.讀鎖戳且寫鎖可得到,則釋放讀鎖,返回寫鎖戳
57              * 3.樂觀讀戳,噹噹即可用時返回寫鎖戳
58              * 4.其餘狀況返回0
59              */
60             long ws = sl.tryConvertToWriteLock(stamp);
61             //若是寫鎖成功
62             if (ws != 0L) {
63               stamp = ws;// 替換票據爲寫鎖
64               x = newX;//修改數據
65               y = newY;
66               break;
67             }
68             //轉換爲寫鎖失敗
69             else {
70                 //釋放讀鎖
71                 sl.unlockRead(stamp);
72                 //獲取寫鎖(必要狀況下阻塞一直到獲取寫鎖成功)
73                 stamp = sl.writeLock();
74             }
75           }
76         } finally {
77             //釋放鎖(多是讀/寫鎖)
78             sl.unlock(stamp);
79         }
80     }
81 }

 

原理

StampedLockd的內部實現是基於CLH鎖的,一種自旋鎖,保證沒有飢餓且FIFO。

CLH鎖原理:鎖維護着一個等待線程隊列,全部申請鎖且失敗的線程都記錄在隊列。一個節點表明一個線程,保存着一個標記位locked,用以判斷當前線程是否已經釋放鎖。當一個線程試圖獲取鎖時,從隊列尾節點做爲前序節點,循環判斷全部的前序節點是否已經成功釋放鎖。

如上圖所示,StampedLockd源碼中的WNote就是等待鏈表隊列,每個WNode標識一個等待線程,whead爲CLH隊列頭,wtail爲CLH隊列尾,state爲鎖的狀態。long型即64位,倒數第八位標識寫鎖狀態,若是爲1,標識寫鎖佔用!下面圍繞這個state來說述鎖操做。

首先是常量標識:

WBIT=1000 0000(即-128)

RBIT =0111 1111(即127) 

SBIT =1000 0000(後7位表示當前正在讀取的線程數量,清0)

1.樂觀讀

tryOptimisticRead():若是當前沒有寫鎖佔用,返回state(後7位清0,即清0讀線程數),若是有寫鎖,返回0,即失敗。

2.校驗stamp

校驗這個戳是否有效validate():比較當前stamp和發生樂觀鎖獲得的stamp比較,不一致則失敗。

3.悲觀讀

樂觀鎖失敗後鎖升級爲readLock():嘗試state+1,用於統計讀線程的數量,若是失敗,進入acquireRead()進行自旋,經過CAS獲取鎖。若是自旋失敗,入CLH隊列,而後再自旋,若是成功得到讀鎖,激活cowait隊列中的讀線程Unsafe.unpark(),最終依然失敗,Unsafe().park()掛起當前線程。

4.排它寫

writeLock():典型的cas操做,若是STATE等於s,設置寫鎖位爲1(s+WBIT)。acquireWrite跟acquireRead邏輯相似,先自旋嘗試、加入等待隊列、直至最終Unsafe.park()掛起線程。

5.釋放鎖

unlockWrite():釋放鎖與加鎖動做相反。將寫標記位清零,若是state溢出,則退回到初始值;

性能和建議JDK8以後纔有,當高併發下且讀遠大於寫時,因爲能夠樂觀讀,性能極高!

5.總結

4種鎖,最穩定是內置synchronized鎖(並非徹底被替代),當併發量大且讀遠大於寫的狀況下最快的的是StampedLock鎖(樂觀讀。近似於無鎖)。建議你們採用。

====================

參考:《JAVA高併發程序設計》

相關文章
相關標籤/搜索