說完了我們的synchronized,這次我們來說說我們的顯示鎖ReetrantLock。
上期回顧:
上次博客我們主要說了鎖的分類,synchronized的使用,和synchronized隱式鎖的膨脹升級過程,從無鎖是如何一步步升級到我們的重量級鎖的,還有我們的逃逸分析。
鎖的粗化和鎖的消除
這個本來應該是在synchronized里面去說的,忘記了,不是很重要,但是需要知道有這么一個東西啦。
我們先來演示一下鎖的粗化:
StringBuffer sb = new StringBuffer(); public void lockCoarseningMethod(){ //jvm的優化,鎖的粗化 sb.append("1"); sb.append("2"); sb.append("3"); sb.append("4"); }
我們都知道我們的StringBuffer是線程安全的,也就是說我們的StringBuffer是用synchronized修飾過的。那么我們可以得出我們的4次append都應該是套在一個synchronized里面的。
StringBuffer sb = new StringBuffer(); public void lockCoarseningMethod() { synchronized (Test.class) { sb.append("1"); } synchronized (Test.class) { sb.append("2"); } synchronized (Test.class) { sb.append("3"); } synchronized (Test.class) { sb.append("4"); } }
按照理論來說應該是這樣的,其實JVM對synchronized做了優化處理,底層會優化成一次的synchronized修飾,感興趣的可以用javap -c 自己看一下,這里就不帶大家去看了,我以前的博客有javap看匯編指令碼的過程。
StringBuffer sb = new StringBuffer(); public void lockCoarseningMethod() { synchronized (Test.class) { sb.append("1"); sb.append("2"); sb.append("3"); sb.append("4"); } }
再來看一下鎖的消除,其實這個鎖的消除,真的對於synchronized理解了,鎖的消除一眼就知道是什么了。
public static void main(String[] args) { synchronized (new Object()){ System.out.println("開始處理邏輯"); } }
對於synchronized而言,我們每次去鎖的都是對象,而你每次都創建的一個新對象,那還鎖毛線了,每個線程都可以拿到對象,都可以拿到對象鎖啊,所以沒不會產生鎖的效果了。
概述AQS:
AQS是AbstractQueuedSynchronizer的簡稱,字面意思,抽象隊列同步器。Java並發編程核心在於java.concurrent.util包而juc當中的大多數同步器 實現都是圍繞着共同的基礎行為,比如等待隊列、條件隊列、獨占獲取、共享獲 取等,而這個行為的抽象就是基於AbstractQueuedSynchronizer簡稱AQS,AQS定 義了一套多線程訪問共享資源的同步器框架,是一個依賴狀態(state)的同步器。就是我們上次博客說的什么公平鎖,獨占鎖等等。
- 阻塞等待隊列
- 共享/獨占
- 公平/非公平
- 可重入
- 允許中斷
AQS的簡單原理解讀:
ReetrantLock的內部功能還是很強大的,有很多的功能,我們來一點點縷縷。如Lock,Latch,Barrier等,都是基於AQS框架實現,一般通過定義內部類Sync繼承AQS將同步器所有調用都映射到Sync對應的方法AQS內部維護屬性volatile int state (32位),state表示資源的可用狀態
- State三種訪問方式
- getState()
- setState()
- compareAndSetState()
- AQS定義兩種資源共享方式
- Exclusive-獨占,只有一個線程能執行,如ReentrantLock
- Share-共享,多個線程可以同時執行,如Semaphore/CountDownLatch
- AQS定義兩種隊列
- 同步等待隊列
- 條件等待隊列
- AQS已經在頂層實現好了。自定義同步器實現時主要實現以下幾種方法:
- isHeldExclusively():該線程是否正在獨占資源。只有用到condition才需要去實現它。
- tryAcquire(int):獨占方式。嘗試獲取資源,成功則返回true,失敗則返回false。
- tryRelease(int):獨占方式。嘗試釋放資源,成功則返回true,失敗則返回false。
- tryAcquireShared(int):共享方式。嘗試獲取資源。負數表示失敗;0表示成功,但沒有剩余可用資源;正數表示成功,且有剩余資源。
- tryReleaseShared(int):共享方式。嘗試釋放資源,如果釋放后允許喚醒后續等待結點返回true,否則返回false。
剛才提到那么多屬性,可能會有一些懵,我們來看一下ReentrantLock內部是怎么來實現哪些鎖的吧。
打開我們的ReetrantLock源代碼可以看到一個關鍵的屬性
private final Sync sync;
后面有一個抽象方法並且繼承了AbstractQueuedSynchronizer類,內部有一個用volatile修飾過的整型變量state,他就是用來記錄上鎖次數的,這樣就實現了我們剛才的說的重入鎖和非可重入鎖。我們來畫一個圖。
AbstractQueuedSynchronizer這個類里面定義了詳細的ReetrantLock的屬性,后面我會一點點去說,帶着解讀一下源碼(上面都是摘自源碼的)。state和線程exclusiveOwnerThread比較好理解,最后那個隊列可能不太好弄,我這里寫的也是比較泛化的,后面我會弄一個專題一個個去說。 相面說的CLH隊列其實不是很准確,我們可以理解為就是一個泛型為Node的雙向鏈表結構就可以了。
等待隊列中Node節點內還有三個很重要的屬性就是prev前驅指針指向我們的前一個Node節點,和一個next后繼指針來指向我們的下一個Node節點,這樣就形成了一個雙向鏈表的結構,於此同時還有一個Thread來記錄我們的當前線程。
在條件隊列中,prev和next指針都是null的,不管是什么隊列,他都有一個waitStatus的屬性來記錄我們的節點狀態的,就是我們剛才說的CANCELLED結束、SIGNAL可喚醒那四個常量值。
AQS中ReetrantLock的使用:
公平鎖和非公平鎖:這個還是比較好記憶的,舉一個栗子,我們去車站排隊上車,總有**插隊,用蛇形走位可以上車的是吧,這就是一個非公平的鎖,如果說,我們在排隊的時候加上護欄,每次只能排一個人,他人無法插隊的,這時就是一個公平鎖。總之就是不加塞的就是公平的,我們都討厭不公平。
重入鎖與非可重入鎖:這個也很好理解,重入鎖就是當我們的線程A拿到鎖以后,可以繼續去拿多把鎖,然后再陸陸續續的做完任務再去解鎖,非可重入呢,就是只能獲得一把鎖,如果想獲取多把鎖,不好意思,去后面排下隊伍。下面我化了一個重入鎖的栗子,快過年了,大家提着行李回老家,我們進去了會一並帶着行李進去(不帶行李的基本是行李丟了),這就是一個重入鎖的栗子,我們人進去了獲得通道通過(鎖),然后我們也拖着行李獲得了通道通過(鎖),然后我們才空出通道供后面的人使用。如果是非可重入鎖就是人進去就進去吧,行李再次排隊,說不准什么時候能進來。
上一段代碼來驗證一下我們上面說的那些知識點。
import java.util.concurrent.locks.ReentrantLock; public class Test { private ReentrantLock lock = new ReentrantLock(true);//true公平鎖,false非公平鎖 public void lockMethod(String threadName) { lock.lock(); System.out.println(threadName + "得到了一把鎖1"); lock.lock(); System.out.println(threadName + "得到了一把鎖2"); lock.lock(); System.out.println(threadName + "得到了一把鎖3"); lock.unlock(); System.out.println(threadName + "釋放了一把鎖1"); lock.unlock(); System.out.println(threadName + "釋放了一把鎖2"); lock.unlock(); System.out.println(threadName + "釋放了一把鎖3"); } public static void main(String[] args) { Test test = new Test(); new Thread(() -> { String threadName = Thread.currentThread().getName(); test.lockMethod(threadName); }, "線程A").start(); } }
通過代碼閱讀我們知道我們弄一個重入鎖,加三次鎖,解三次鎖,我們來看一下內部sync的變化,調試一下。
我們看到了我們的state變量是用來存儲我們的入鎖次數的。剛才去看過源碼的小伙伴知道了我們的state是通過volatile修飾過的,雖然可以保證我們的有序性和可見性,但是一個int++的操作,他是無法保證原子性的,我們繼續來深挖一下代碼看看內部是怎么實現高並發場景下保證數據准確的。點擊lock方法進去,我們看到lock方法是基於sync來操作的,就是我們上面的畫的那個ReetrantLock的圖。
/** * Sync object for fair locks */ static final class FairSync extends Sync { private static final long serialVersionUID = -3000897897090466540L; final void lock() {//開始加鎖 acquire(1); } /** * Fair version of tryAcquire. Don't grant access unless * recursive call or no waiters or is first. */ protected final boolean tryAcquire(int acquires) { final Thread current = Thread.currentThread();//得到當前線程 int c = getState();//得到上鎖次數 if (c == 0) {//判斷是否上過鎖 if (!hasQueuedPredecessors() &&//hasQueuedPredecessors判斷是否有正在等待的節點, compareAndSetState(0, acquires)) {//通過unsafe去更新上鎖次數 setExclusiveOwnerThread(current);//設置線程 return true; } } else if (current == getExclusiveOwnerThread()) { int nextc = c + acquires; if (nextc < 0) throw new Error("Maximum lock count exceeded"); setState(nextc); return true; } return false; } }
這次我們開啟多個線程來同時訪問來看一下我們的Node的變化。同時開啟ABCD四個線程來執行這個
這次我們看到了head屬性和tail屬性不再是空的。head是也是一個node節點,前驅指針是空的,后驅指針指向后繼節點,Thread為空,tail的node節點正好是和head相對應的節點。這樣的設計就是為了更好的去驗證隊列中還是否存在剩余的線程節點需要處理。然后該線程運行結束以后會喚醒在隊列中的節點,然其它線程繼續運行。
我們知道我們創建的公平鎖,如果說BCD好好的在排隊,E線程來了,只能好好的去排隊,因為公平,所以排隊,如果我們創建的是非公平鎖,E線程就有機會拿到鎖,拿到就運行,拿不到就去排隊。