為了更好的支持並發程序,JDK內部提供了多種鎖。本文總結4種鎖。
1.synchronized同步鎖
使用:
synchronized本質上就2種鎖:
1.鎖同步代碼塊
2.鎖方法
可用object.wait() object.notify()來操作線程等待喚醒
原理:synchronized細節的描述傳送門:jdk源碼剖析三:鎖Synchronized
性能和建議:JDK6之后,在並發量不是特別大的情況下,性能中等且穩定。建議新手使用。
2.ReentrantLock可重入鎖(Lock接口)
使用:ReentrantLock是Lock接口的實現類。Lock接口的核心方法是lock(),unlock(),tryLock()。可用Condition來操作線程:

如上圖,await()和object.wait()類似,singal()和object.notify()類似,singalAll()和object.notifyAll()類似
原理:核心類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次。
性能和建議:適用於讀多寫少的情況。性能較高。
- 公平性
- 非公平鎖(默認),為了防止寫線程餓死,規則是:當等待隊列頭部結點是獨占模式(即要獲取寫鎖的線程)時,只有獲取獨占鎖線程可以搶占,而試圖獲取共享鎖的線程必須進入隊列阻塞;當隊列頭部結點是共享模式(即要獲取讀鎖的線程)時,試圖獲取獨占和共享鎖的線程都可以搶占。
- 公平鎖,利用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高並發程序設計》
