爲了更好的支持併發程序,JDK內部提供了多種鎖。本文總結4種鎖。html
使用:併發
synchronized本質上就2種鎖:高併發
1.鎖同步代碼塊性能
2.鎖方法ui
可用object.wait() object.notify()來操做線程等待喚醒spa
原理:synchronized細節的描述傳送門:jdk源碼剖析三:鎖Synchronized 線程
性能和建議:JDK6以後,在併發量不是特別大的狀況下,性能中等且穩定。建議新手使用。設計
使用:ReentrantLock是Lock接口的實現類。Lock接口的核心方法是lock(),unlock(),tryLock()。可用Condition來操做線程:code
如上圖,await()和object.wait()相似,singal()和object.notify()相似,singalAll()和object.notifyAll()相似orm
原理:核心類AbstractQueuedSynchronizer,經過構造一個基於阻塞的CLH隊列容納全部的阻塞線程,而對該隊列的操做均經過Lock-Free(CAS)操做,但對已經得到鎖的線程而言,ReentrantLock實現了偏向鎖的功能。
性能和建議:性能中等,建議須要手動操做線程時使用。
使用:ReentrantReadWriteLock是ReadWriteLock接口的實現類。ReadWriteLock接口的核心方法是readLock(),writeLock()。實現了併發讀、互斥寫。但讀鎖會阻塞寫鎖,是悲觀鎖的策略。
原理:類圖以下:
JDK1.8下,如圖ReentrantReadWriteLock有5個靜態方法:
性能和建議:適用於讀多寫少的狀況。性能較高。
UnsupportedOperationException
異常。應用:
使用:
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以後纔有,當高併發下且讀遠大於寫時,因爲能夠樂觀讀,性能極高!
4種鎖,最穩定是內置synchronized鎖(並非徹底被替代),當併發量大且讀遠大於寫的狀況下最快的的是StampedLock鎖(樂觀讀。近似於無鎖)。建議你們採用。
====================
參考:《JAVA高併發程序設計》