轉載請注明出處。。。。。
一、介紹
大家都知道,在java中如果要對一段代碼做線程安全操作,都用到了鎖,當然鎖的實現很多,用的比較多的是sysnchronize和reentrantLock,前者是java里的一個關鍵字,后者是一個java類。這兩者的大致區別,在這里羅列下
相同點:
1、都能保證了線程安全性
2、都支持鎖的重入
不同點:
1、synchronized適用於不是很激烈的情況,reentranLock適用於比較競爭激烈的情況
2、Synchronized是jvm層面實現的鎖機制,而reentranLock是java代碼層面實現的鎖機制。
3、Reentranlock比synchronized多了鎖投票,定時鎖,中斷鎖等機制
4、synchronized是隱式獲取鎖和釋放鎖,不需要代碼手動獲取釋放,Reentranlock為顯示獲取鎖和釋放鎖,必須要手動代碼獲取釋放
要了解reentranlock,那肯定先得會用它,下面通過一個例子來了解它的加鎖和釋放鎖過程
二、demo
1 public class Demo { 2 3 private static int count = 0; 4 5 public static void main(String[] args) throws InterruptedException { 6 ExecutorService executorService = Executors.newFixedThreadPool(15); 7 for (int i = 0; i < 500; i++){ 8 executorService.execute(() -> { 9 add(); 10 }); 11 } 12 Thread.sleep(1000); 13 System.out.println(count); 14 } 15 16 private static int add(){ 17 return ++count; 18 } 19 }
上述代碼,安裝預期結果 那肯定是500,但是真的是500嗎?結果如下
結果很顯然,它是小於500的,把這段代碼用鎖保證結果和預期結果一致。代碼如下
1 public class Demo { 2 3 private static int count = 0; 4 5 private static Lock lock = new ReentrantLock(); 6 7 public static void main(String[] args) throws InterruptedException { 8 ExecutorService executorService = Executors.newFixedThreadPool(15); 9 for (int i = 0; i < 500; i++){ 10 executorService.execute(() -> { 11 add(); 12 }); 13 } 14 Thread.sleep(1000); 15 System.out.println(count); 16 } 17 18 private static int add(){ 19 lock.lock(); 20 try { 21 return ++count; 22 }finally { 23 lock.unlock(); 24 } 25 26 } 27 }
結果,和預期一致。
那它是怎么保證線程安全性的呢。往下看
三、ReentrantLock分析
先來了解這個類的大致結構
紅框圈中的三個類,其中Sync是一個抽象類,另外兩個是它的子類,Sync又繼承了AQS類,所以它也有鎖的操作可能性。
FairSync是一個公平鎖,NonFairSync是一個非公平鎖,它們雖然繼承了同一個類,但實現上有所不同,
1、非公平鎖獲取鎖的過程
進入lock方法
而sync 是ReentrantLock的一個字段,它在該類的構造函數中初始化,它有兩個構造函數,sync默認為非公平鎖實現,
當sync調用了lock方法,也就是調用NonFairSync類的lock方法,繼續看下去,下圖為該類的結構
lock大致步驟為,先去試着改變state的值,如果改變成功,則state值就變為1了,返回true,失敗返回false,先來解釋下compareAndSetState方法的作用
它有兩個參數,第一個是期望值,第二個是要更新的值,如果內存中state值和期望值相等,則將內存值變為更新值,這是交換成功的標志。如果不相等,那肯定是false。這個方法其實就是CAS,同時它也是線程安全的,具體實現,這里不作討論。
這里也是獲取鎖成功的標志,當返回true,則將獲取鎖的線程置為當前線程,同時state值改變了,如果下一個線程進入,那么該方法肯定是返回false。那么獲取鎖失敗的線程就會進入acquire方法。這個方法其實就是AQS的方法,代碼如下,可以看到它又調用了tryAcquire方法,而這個方法的實現就是上一個圖的nonFairTryAcquire方法,
1 final boolean nonfairTryAcquire(int acquires) { 2 // 獲取當前線程 3 final Thread current = Thread.currentThread(); 4 int c = getState(); 5 // 如果狀態值不為0,則進一步去獲取鎖 6 if (c == 0) { 7 if (compareAndSetState(0, acquires)) { 8 // 獲取鎖成功,將鎖置給當前線程 9 setExclusiveOwnerThread(current); 10 return true; 11 } 12 }// 如果相等,則表明為鎖的重入 13 else if (current == getExclusiveOwnerThread()) { 14 int nextc = c + acquires; 15 if (nextc < 0) // overflow 16 throw new Error("Maximum lock count exceeded"); 17 setState(nextc); 18 return true; 19 } 20 // 只有獲取鎖失敗才會返回false 21 return false; 22 }
當上面返回false時,又會相繼執行addWaiter和acquireQueued方法,其中addWaiter方法主要是將獲取鎖失敗的線程包裝成一個Node節點,插入一個隊列中,注意頭結點不是該節點,而是new了一個新的node節點,它的狀態值為0,然后返回該節點。
具體代碼不做分析,下面看acquireQueued方法
1 final boolean acquireQueued(final Node node, int arg) { 2 boolean failed = true; 3 try { 4 boolean interrupted = false; 5 // 類似while(true),作無限循環作用 6 for (;;) { 7 // 獲取插入的node節點的前一個節點 8 final Node p = node.predecessor(); 9 // 如果前繼節點為head節點並且獲取鎖成功,則跳出無限循環,執行相應業務代碼 10 if (p == head && tryAcquire(arg)) { 11 setHead(node);// 頭結點被改變,改變同時其狀態也被改變了,節點線程也為空 12 p.next = null; // help GC 13 failed = false; 14 return interrupted; 15 } 16 // 前繼節點不是頭結點或獲取鎖失敗 17 if (shouldParkAfterFailedAcquire(p, node) && 18 parkAndCheckInterrupt()) 19 interrupted = true; 20 } 21 } finally { 22 // 防止代碼運行過程中,線程突然被中斷,中斷則將該節點置為取消狀態 23 if (failed) 24 cancelAcquire(node); 25 } 26 }
其中shouldParkAfterFailedAcquire方法做了這兩件事,
1、如果p節點前有狀態為cancel的節點,則將這些取消的節點放棄掉,簡單來說就是排除取消的節點
2、將p節點狀態置為signal狀態。等待下一次進入該方法可能會被掛起
方法parkAndCheckInterrupt,在shouldParkAfterFailedAcquire返回true的時候,線程會被掛起。、
以上就是獲取鎖的過程,步驟如下
1、獲取鎖成功,則將改變state值,並將鎖的擁有者置為當前線程
2、獲取鎖失敗,則進入同步隊列中,直到獲取鎖成功或當前線程被外因給中斷,獲取鎖的過程中,有的線程可能會被掛起。
2、非公平鎖釋放鎖的過程
為了不顯得過於啰嗦,下面只列出核心代碼
上述代碼只有獲取鎖的線程調用了unlock方法,才會去修改state值,當state值為0時其他線程又可以獲取鎖,看到這,或許有的小伙伴迷糊了,上面不是介紹說在獲取鎖的過程中,有的線程會被掛起,那如果掛起的線程node節點前繼恰好是頭結點,那豈不是運行不了?,莫慌,往下看。當state值置為0時,該方法會返回true,之后會執行下面方法。
重點方法在unparkSuccessor方法上,看if(h != null && h.waitStatus !=0) ,為什么要加這個判斷呢,因為如果有多個線程在獲取鎖,無論是獲取失敗,還是獲取成功head節點的狀態值都被改變(setHead()和shouldParkAfterFailedAcquire()方法會去改變head節點狀態)。即不為0,如果為0,那么就說明就沒有線程被掛起,自然就不用去釋放這些線程。加這個判斷,為了減少無用操作。重點來了,unparkSuccessor方法,代碼如下
private void unparkSuccessor(Node node) { // 將node結點狀態置為0 int ws = node.waitStatus; if (ws < 0) compareAndSetWaitStatus(node, ws, 0); /* * 如果node結點下一個節點為null或被取消則進入下面的for循環 * 下面的for循環從尾節點往前尋找沒有取消的節點 ,直至最靠近node節點,即node節點下一個狀態小於等於0的節點 * 在這里node節點就是頭結點, */ Node s = node.next; if (s == null || s.waitStatus > 0) { s = null; for (Node t = tail; t != null && t != node; t = t.prev) if (t.waitStatus <= 0) s = t; } // 找到了該節點,釋放該節點的線程 if (s != null) LockSupport.unpark(s.thread); }
或許看到這更迷糊了,它釋放鎖怎么能確定釋放的就是那個被掛起的線程呢,這個呢,確實確定不了,但是如果釋放前繼節點為頭結點的線程,那么在后續獲取鎖的過程中,該線程肯定能獲取到鎖(因為這段代碼是前一個線程釋放鎖的操作代碼,所以下一個線程肯定能獲取到鎖),至此又一輪循環。
在這里,我對那個為啥從尾節點向前遍歷也不清楚,如果有清楚的小伙伴,還請評論下方留言,謝謝!
以上就是非公平鎖的釋放操作。
3、公平鎖的獲取鎖過程
該種鎖和非公平鎖的不同之處,就是這種鎖一定得按照順序來獲取或,不能前一個線程釋放了鎖 ,然后誰搶到了就算誰的。
先來看下這種獲取鎖的代碼
1 protected final boolean tryAcquire(int acquires) { 2 final Thread current = Thread.currentThread(); 3 int c = getState(); 4 if (c == 0) { 5 if (!hasQueuedPredecessors() && 6 compareAndSetState(0, acquires)) { 7 setExclusiveOwnerThread(current); 8 return true; 9 } 10 } 11 else if (current == getExclusiveOwnerThread()) { 12 int nextc = c + acquires; 13 if (nextc < 0) 14 throw new Error("Maximum lock count exceeded"); 15 setState(nextc); 16 return true; 17 } 18 return false; 19 }
和非公平鎖的不同點是在前者線程釋放鎖后(即state值為0),非公平鎖是誰搶到鎖,鎖就是誰的,但是公平鎖不一樣,獲取鎖的線程會先去判斷同步隊列中有沒有其他線程,如果沒有,再去試着改變state值,如果改變成功則獲取鎖成功,它不允許沒進入同步隊列中的線程(此時同步隊列中已有等待的線程,如果沒有,那就是直接搶)搶占鎖。下面看下hasQueuedPrdecessor(),代碼如下
1 public final boolean hasQueuedPredecessors() { 2 // The correctness of this depends on head being initialized 3 // before tail and on head.next being accurate if the current 4 // thread is first in queue. 5 Node t = tail; // Read fields in reverse initialization order 6 Node h = head; 7 Node s; 8 return h != t && 9 ((s = h.next) == null || s.thread != Thread.currentThread()); 10 }
代碼不復雜,就是判斷同步隊列中有沒有等待的線程,且等待的線程不是當前線程,有則返回true,沒有則返回false。
至於公平鎖的釋放操作,和非公平鎖一致。這里不過多敘述。
獲取公平鎖操作
1、先判斷同步隊列中有沒有等待的線程。
2、有則放棄鎖的爭奪,進入同步隊列排好隊,沒有則搶占鎖
----------------------------------------------------------------------------------------------------華麗的分界線---------------------------------------------------------------------------------------------------------------------------------
本來想繼續寫condition,但好像篇幅有點啰嗦,就放在下一篇。
以上就是我的個人見解,如果不足或錯誤之處,還請指教,謝謝!