java架構之路(多線程)AQS之ReetrantLock顯示鎖的使用和底層源碼解讀
說完了我們的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裡面的。
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看彙編指令碼的過程。
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具備特性
阻塞等待隊列
共享/獨占
公平/非公平
可重入
允許中斷
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線程就有機會拿到鎖,拿到就運作,拿不到就去排隊。
原文位址
https://www.cnblogs.com/cxiaocai/p/12191666.html