一、前言
Java線程同步兩種方式,synchronized關鍵字和Lock鎖機制,其中,AQS隊列就是Lock鎖實現公平加鎖的底層支持。
二、AQS源碼對於lock.lock()的實現
2.1 AQS類 + 內部Node類
2.1.1 AQS類結構示意圖
首先我們要看看AQS的結構的類圖
從AQS類的類結構示意圖可以知道,
- AbstractQueuedSynchronizer的父類是AbstractOwnableSynchronizer;
- AbstractQueuedSynchronizer的子類是Sync,然后又通過繼承Sync得到了FairSync公平鎖和UnfaiSync非公平鎖,所以,AQS是Lock鎖機制實現公平鎖的底層支持。
2.1.2 內部Node類
通過上面的結構圖,我們可以知道該類維護了一個Node內部類,於是我們查看Node的源碼如下,主要是用來實現上面的我們提到的隊列。
static final class Node {
//指示節點正在共享模式下等待的標記
static final Node SHARED = new Node();
//指示節點正在以獨占模式等待的標記
static final Node EXCLUSIVE = null;
//waitStatus值,指示線程已取消 cancel
// 這個節點已經被取消 canceled 這樣可讀性強
static final int CANCELLED = 1;
//waitStatus值,指示后續線程需要釋放 signal
// 這個節點的后繼被阻塞,因此當前節點在取消必須釋放它的后繼
static final int SIGNAL = -1;
//waitStatus值,指示線程正在等待條件 condition
// 這個節點在條件隊列里面
static final int CONDITION = -2;
//waitStatus值,表示下一個被默認的應該無條件傳播的等待狀態值 propagate
static final int PROPAGATE = -3;
/*
* SIGNAL:這個節點的后繼被(或即將)阻塞(通過park),因此當前節點在釋放或取消時必須釋放它的后繼。為了避免競爭,acquire方法必須首先表明它們需要一個信號,然后重試原子獲取,當失敗時,阻塞。
*
* CANCELLED:由於超時或中斷,該節點被取消。節點不會離開這個狀態。特別是,取消節點的線程不會再次阻塞。
*
* CONDITION:此節點當前處於條件隊列中。在傳輸之前,它不會被用作同步隊列節點,此時狀態將被設置為0。
*
* PROPAGATE:釋放的共享應該傳播到其他節點。在doReleaseShared中設置這個(僅針對頭節點),以確保傳播繼續,即使其他操作已經干預。
*
* 0:以上都不是
*/
volatile int waitStatus; // 默認值0,什么都不是
//上一個節點
volatile Node prev;
//下一個節點
volatile Node next;
//節點中的值
volatile Thread thread;
//下一個等待節點
Node nextWaiter;
//判斷是否是共享的節點
final boolean isShared() {
return nextWaiter == SHARED;
}
//返回當前的節點前置節點
final Node predecessor() throws NullPointerException {
Node p = prev;
if (p == null)
throw new NullPointerException();
else
return p;
}
//用於建立初始標頭或SHARED標記
Node() {
}
//addWaiter時候調用
Node(Thread thread, Node mode) {
this.nextWaiter = mode;
this.thread = thread;
}
//Condition時候調用
Node(Thread thread, int waitStatus) {
this.waitStatus = waitStatus;
this.thread = thread;
}
}
復制代碼
關於waitStatus:
- SIGNAL=-1:這個節點的后繼被(或即將)阻塞(通過park),因此當前節點在釋放或取消時必須釋放它的后繼。為了避免競爭,acquire方法必須首先表明它們需要一個信號,然后重試原子獲取,當失敗時,阻塞。
- CANCELLED=1:由於超時或中斷,該節點被取消。節點不會離開這個狀態。特別是,取消節點的線程不會再次阻塞。
- CONDITION=-2:此節點當前處於條件隊列中。在傳輸之前,它不會被用作同步隊列節點,此時狀態將被設置為0。
- PROPAGATE=-3:釋放的共享應該傳播到其他節點。在doReleaseShared中設置這個(僅針對頭節點),以確保傳播繼續,即使其他操作已經干預。
- waitStatus=0:以上都不是
根據上面代碼,知道AQS隊列是一個雙鏈表實現的隊列,每個節點包含prev指針和next指針,具體如下圖:
問題:AQS內部類Node
回答:AQS本質是一個非循環的雙向鏈表(也可以稱為隊列),所以它是由一個個節點構成的,就是Node,后面的lock() unlock() await() signal()/signalAll()都是以Node為基本元素操作的。
問題:AQS類中的Node內部類中需要保存什么信息呢?
回答:一個六個,其中,prev、next 兩個Node類型,表示做指針,thread 存放節點的值,因為AQS隊列的節點就是存放線程的,所以這個值類型就是Thread,最后,nextWaiter也是Node類型,表示下一個等待節點, waitStatus表示當前節點等待狀態,SHARED|EXCLUSIVE 表示是獨占還是共享。
volatile int waitStatus; //當前節點等待狀態
volatile Node prev; //上一個節點
volatile Node next; //下一個節點
volatile Thread thread; //節點中的值
Node nextWaiter; //下一個等待節點
//指示節點共享還是獨占,默認初始是共享
static final Node SHARED = new Node();
static final Node EXCLUSIVE = null;
復制代碼
記住一個Node節點的六個屬性(共享/獨占算一個),下面看源碼就輕松些
- 處在同步隊列中使用到的屬性(本文用:加鎖、解鎖)包括:next prev thread waitStatus,所以同步隊列是雙向非循環鏈表,涉及的類變量AbstractQueuedSynchronizer類中的head和tail,分別指向同步隊列中的頭結點和尾節點。
- 處在等待隊列中使用到的屬性(下一篇博文用:阻塞、喚醒)包括:nextWaiter thread waitStatus,所以等待隊列是單向非循環鏈表,涉及的類變量ConditionObject類中的firstWaiter和lastWaiter,分別指向等待隊列中的頭結點和尾節點。
- AQS隊列是工作隊列、同步隊列,是非循環雙向隊列:當使用到head tail的時候,就說AQS隊列建立起來了,單個線程不使用到head tail,所以AQS隊列沒有建立起來;
- 等待隊列,是非循環單向隊列:當使用firstWaiter lastWaiter的時候,就說等待隊列建立起來了。
- lock()和unlock()就是操作同步隊列:lock()將線程封裝到節點里面(此時,節點使用到的屬性是thread nextWaiter waitStatus),放到同步隊列,即AQS隊列中,unlock()將存放線程的節點從同步隊列中拿出來,表示這個線程工作完成。
- await()和signal()就是操作等待隊列:await()將線程封裝到節點里面(此時,節點使用到的屬性是thread prev next waitStatus),放到等待隊列里面,signal()從等待隊列中拿出元素。
2.2 公平鎖加鎖需要工作隊列:FairSync中的lock()方法(重點)
2.2.1 lock方法只有一個線程的情況
2.2.1.1 整體路程圖(第1次加鎖 + 第2~n次加鎖)
lock方法只有一個線程的情況(ps:此時還沒有AQS隊列,head==tail),如下圖所示:
對於上圖的解釋,看這張圖的正確姿勢是:
- 上圖中涉及的方法包括 acquire() 、 tryAcquire() 、 hasQueuedPredecessors()方法;
- 對於acquire() 方法,就是直接調用 tryAcquire() 方法,實參傳入1。
- 對於tryAcquire()方法,由於只有一個線程A,第一次進入的時候要CAS加鎖(if判斷后半段)並將自己設置為獨占線程(if代碼段),所以兩行代碼,以后就是重入鎖了,直接進入,不用CAS操作和設置獨占鎖了,直接通過setState()修改state值,每次加一即可,因為實參acquires為1。
- 對於hasQueuedPredecessors()方法,該方法就是判斷AQS工作隊列是否建立起來了,這里h==t,所以,沒有建立起來,返回false。
- 總體流程,對於同一線程A,對於圖中唯一一個菱形判斷框,第一次進入的時候要CAS加鎖(if判斷后半段)並將自己設置為獨占線程(if代碼段);第2~n次進入就是重入鎖,直接進入,不用CAS操作和設置獨占鎖了,直接通過setState()修改state值,每次加一即可,這就是整體流程,線程A的整體流程,分為兩種球情況而已。
公平鎖加鎖流程(只有一個線程的時候):
- 直接調用tryAcquire,然后判斷state的是不是等於0
- 對於A線程第1次加鎖,等於0,證明是第一次加鎖,第一次加鎖hasQueuedPredecessors()一定通過(返回為false取反為true),通過CAS操作將state的值改成1,然后true,返回true表示加鎖成功,就完成了加鎖。
- 對於A線程第2~n次加鎖,不等於0,表示不是第一次加鎖,這個鎖是重入鎖,這個時候將原來的state值繼續通過CAS操作加上1,再次返回true,表示加鎖成功,就完成了加鎖。
tip1:需要注意的是,這個時候AQS的隊列沒有創建出來。
tip2:setExclusiveOwnerThread(current); // 這里是設置當前節點為獨占 記住上面六個屬性
tip3:看源碼的時候,知道自己在看什么,這里是看FairSync的lock()方法實現
tip4:源碼一般命名優美,可以從命名上來看,幫助理清思路,例如 lock()是加鎖、acquire()是去獲得tryAcquire() 是嘗試加鎖、acquireQueued()是獲得隊列
2.2.1.2 重要方法:tryAcquire()源碼解析
對於只有一個線程A使用lock.lock();加鎖,最重要的方法就是tryAcquire,就是這個方法,將線程A區分對待,第一次加鎖和非第一次加鎖,源碼拿出來講一講:
protected final boolean tryAcquire(int acquires) { // **1、tryAcquire是去獲取,2、返回為true就是使用獲取的方式加鎖成功(可以第一次,也可以是重入鎖)**
final Thread current = Thread.currentThread();
int c = getState(); // 當前狀態
if (c == 0) { // 當前狀態為0,就是默認狀態
if (!hasQueuedPredecessors() && // **1、hasQueuedPredecessors這個方法重要,下面解釋**
compareAndSetState(0, acquires)) { // **1、只要上面那個hasQueuedPredecessors()返回為false,取反為true,這個cas一定是可以通過的,只是自旋等一下罷了**
setExclusiveOwnerThread(current); // **1、設置當前線程為獨占線程,因為當前線程已經加鎖成功了,所以設置當前線程為互斥資源的獨占線程**
//**2、為什么說當前線程加鎖成功了,因為這里返回true啊**
return true;
}
}
else if (current == getExclusiveOwnerThread()) { // 這句表示當前線程為獨占線程
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true; // **1、因為當前線程是獨占線程,所以一定是加鎖成功,這里返回true就好
// 2、既然已經是獨占線程,就沒有必要再次設置當前線程為獨占線程了,直接返回true**
}
return false; // **1、如果既不是第一次,也不是重入鎖,就不能通過獲取的方式去加鎖,要自己加鎖,這里返回false,加鎖失敗**
}
復制代碼
2.2.1.3 重要方法:hasQueuedPredecessors()源碼解析
public final boolean hasQueuedPredecessors() {
Node t = tail; // Read fields in reverse initialization order
Node h = head;
Node s;
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
復制代碼
問題:hasQueuedPredecessors()源碼解析?
回答:
-
如果h == t成立,h和t均為null或是同一個具體的節點,無后繼節點,方法返回false,表示要通過獲取鎖tryLock()加鎖,這種情況是第一個節點,類變量tail == head == null。
key:如果類變量head==tail,表示沒有節點或只有一個節點,所以一定是沒有前驅節點的,方法直接返回false,不用多說,注意,后面的,head!=tail至少兩個節點 -
如果h!=t成立(至少兩個節點),而且 head.next == 為null,方法返回true。什么情況下h!=t的同時h.next==null??有其他線程第一次正在入隊時,可能會出現。見AQS的enq方法,compareAndSetHead(node)完成,還沒執行tail=head語句時,此時tail=null,head=newNode,head.next=null。
key:此時,頭結點設置為新建節點,所以head=newNode
但是,還未將頭結點設置為尾節點,所以tail=null,為默認值
同時,這是第一次執行enq()方法,沒有設置 node.prev = t; 和 t.next = node;,所以head.next=null。 -
如果h!=t成立(當前類變量head 和 tail不是同一個,至少兩個節點),head.next != null,(true && (false || ))則判斷head.next是否是當前線程,如果是當前線程(true && (false || false)),方法返回false,key:表示要通過獲取鎖的方式加鎖;如果不是當前線程(true && (false || true))返回true,key:表示正在運行的線程不是當前的這個current,需要等待。
(head節點是獲取到鎖的節點,但是任意時刻head節點可能占用着鎖,也可能釋放了鎖(unlock()),未被阻塞的head.next節點對應的線程在任意時刻都是有必要去嘗試獲取鎖)
實際上,hasQueuedPredecessors返回為true不通過,只需要等一段時間罷了(上面關於hasQueuedPredecessors方法的意義:如果該方法返回true,則表示有線程比當前線程更早地請求獲取鎖,因此需要等待前驅線程獲取並釋放鎖之后才能繼續獲取鎖)
問題:hasQueuedPredecessors()方法是用來干什么的?
回答:
- hasQueuedPredecessors()方法的意義:該方法表示對加入了同步隊列中當前節點是否有前驅節點的判斷,如果該方法返回true,則表示有線程比當前線程更早地請求獲取鎖,因此需要等待前驅線程獲取並釋放鎖之后才能繼續獲取鎖,因為是公平鎖,要按照隊列來。
- hasQueuedPredecessors()方法的調用以及返回的意義:hasQueuedPredecessors()方法只在tryAcquire()方法里面被調用執行過,hasQueuedPredecessors()返回false表示要嘗試通過獲取鎖的方式加鎖,沒有前驅節點請求獲取鎖,返回為true表示不需要通過獲取鎖的方式加鎖,已經有前驅節點請求獲取了。
問題:如何進入hasQueuedPredecessors()方法?
回答:lock() -> acquire() -> tryAcquire() -> hasQueuedPredecessors()
2.2.2 lock方法中有兩個線程的情況
2.2.2.1 整體路程圖(線程A加鎖 + 線程B加鎖)
lock方法實現的公平鎖AQS隊列中有兩個線程的情況,如下圖所示:
上圖注意兩點:
- 流程:兩個線程的運行,左邊是線程A先獲得鎖,然后線程B再去嘗試獲得鎖,但是線程B一定要等到線程A釋放鎖之后,才能獲得鎖成功,在這之前只能阻塞,即源碼中for循環;
- 仔細對比兩個圖,和上一次的改變是:線程B節點的上一個節點的waitStatus的值從0修改成-1。
對於上圖的解釋:
先是線程A加鎖,然后線程B加鎖,線程A加鎖沒必要說,和上面一個線程的情況一樣,線程B加鎖分為兩種情況:線程A還沒有解鎖,線程B加鎖失敗;線程A已經解鎖,線程B CAS操作加鎖成功。
2.2.2.2 第一種情況:當線程B進來的時候,死循環中線程A沒有解鎖
第一,建立AQS隊列
我們假設線程A直接獲取到了鎖(獲取鎖的過程和上面單線程一樣,不再贅言),但是線程A還沒有解鎖,這個時候線程B來進行加鎖,走來會執行tryAcquire()方法,這個時候線程A沒有解鎖,所以這個tryAcquire()方法會直接返回false(state!=0,也不是重入鎖),然后會調用addWaiter(Node.EXCLUSIVE)方法(addWaiter()是新方法:上面一個線程的時候沒有涉及到,這里要重點分析),這個時候會在這個方法中的enq(node)的方法中初始化AQS隊列,也會利用尾插法將當前的節點插入到AQS隊列中去。AQS隊列如下圖所示:
對於當前的AQS隊列解釋:
- 這時AQS隊列新建起來了
- head節點中thread=null表示沒有存放任何線程,waitStatus=0表示什么都不是
- tail節點中thread=線程B表示存放的是線程B,waitStatus=0表示什么都不是
第二,addWaiter()方法中調用enq()方法,新建AQS隊列
完成AQS隊列的方法是addWaiter()中調用的enq()方法,且看addWaiter()方法和enq()方法
private Node addWaiter(Node mode) { // **1、實際參數是Node.EXCLUSIVE,就是當前獨占節點,表示下一個等待節點就是正在獨占的那個線程的節點,因為它釋放鎖就要到插入了,所以這個方法稱為addWaiter,意為添加下一個等待節點**
Node node = new Node(Thread.currentThread(), mode); // 新建一個節點,存放當前線程,當前線程為內容,實參為下一個等待節點nextWaiter
Node pred = tail; // 當前尾節點賦值,當前tail==null
if (pred != null) {
node.prev = pred; // 如果不為空,進來,新建節點的前一個是之前的尾節點,就是尾插法
if (compareAndSetTail(pred, node)) { // 設置新的尾節點,從之前的尾節點pred到現在的node
pred.next = node; // 之前尾節點的next設置為這個節點
// **由此可知,尾插法三步驟:設置新節點的prev為之前尾節點、重新設置tail類變量的指向、設置之前尾節點的next為新建節點(就是三個Node類型指針而已,很簡單)**
return node; // 返回新建節點
}
}
enq(node); // 返回值沒有接收者,但是隊列新建好了
return node; // 返回這個新建的節點
}
private Node enq(final Node node) {
for ( ; ; ) { // **1、死循環,不創建好AQS隊列不退出**
Node t = tail;
if (t == null) { // Must initialize **1、第一次進入,必須初始化,這里表示連尾節點都沒有**
if (compareAndSetHead(new Node())) // **for+if(cas)就是線程同步**
tail = head; // **1、新建一個節點,設置為頭結點,因為只有一個節點,所以尾節點也是這個節點**
} else {
node.prev = t; // **1、這是時候,第二次循環,因為head tail都是新節點,第二次循環中使用 Node t = tail;將t設置為這個新節點**
if (compareAndSetTail(t, node)) { // 方法名是compareAndSetTail,表示設置尾節點,自旋,知道設置成功 for+if(cas)就是線程同步,設置tail類變量,將tail從t變為node,所以傳入參數node是尾節點
t.next = node; // 尾節點指向參數node,頭結點還是指向t
// **由此可知,尾插法三步驟:設置參數節點的prev為之前尾節點t、重新設置tail類變量的指向從之前的t到參數節點node、設置之前尾節點t的next為參數節點node(就是三個Node類型指針而已,很簡單),最后隊列兩個元素 t 和 node**
return t; // 返回頭結點t
}
}
}
}
復制代碼
tip:head和tail是類變量,類似指針,指向其他節點
compareAndSetTail(t, node) // 設置tail類變量,將tail從t變為node,所以傳入參數node是尾節點
compareAndSetHead(t, node) // 設置tail類變量,將head從t變為node,所以傳入參數node是頭節點
compareAndSetState(0, acquires) // 設置state類變量,從0到1,cas保證安全
第三,acquireQueued()方法接收addWaiter()返回值,調用shouldParkAfterFailedAcquire()方法,修改AQS隊列中節點的waitStatus值從0到1
方法addWaiter()返回當前的節點,然后調用acquireQueued(addWaiter(Node.EXCLUSIVE), arg))方法(tip:返回的剛剛在addWaiter()方法中新建的最尾巴的節點作為acquireQueued方法的參數,arg參數是1,傳遞過來的)。這個方法中是一個死循環,由於線程A沒有釋放鎖(tryAcquire()方法會直接返回false(state!=0,也不是重入鎖)),會執行shouldParkAfterFailedAcquire(p, node)(p表示線程B節點的上一個節點,p = node.predecessor();就是最尾巴節點上一個,node表示線程B的節點,就是addWaiter()方法中新建的最尾巴的節點)第一次進這個方法會將線程B節點的上一個節點的waitStatus的值改成-1(執行最后一個else compareAndSetWaitStatus(pred, ws, Node.SIGNAL);),然后返回false,這個時候的AQS隊列如下圖:
tip:仔細對比兩個圖,和上一次的改變是:線程B節點的上一個節點的waitStatus的值從0修改成-1。
-
第一次進入shouldParkAfterFailedAcquire()方法會將線程B節點的上一個節點的waitStatus的值改成-1,然后返回false。此時的AQS工作隊列:head節點thread==null表示沒有存放線程,waitStatus=-1表示后面節點的線程需要釋放;tail節點thread=線程B表示存放的是線程B,waitStatus=0表示什么都不是。
-
第二次進入shouldParkAfterFailedAcquire()方法的時候,會返回true( if (ws == Node.SIGNAL) return true;),會執行后面的方法parkAndCheckInterrupt()( LockSupport.park(this);),這個時候線程B就會被park在這。(1、直到線程A解鎖了,第二種情況可以當做第一個情況后面的執行來看)
2.2.2.3 第二種情況:死循環中線程A已經解鎖了
上面的情況都是在線程A沒有解鎖的時候,如果在死循環中線程A已經解鎖了。這個時候判斷線程B節點的上一個節點是不是頭結點,如果是的話,直接執行tryAcquire(),將當前線程B設置成獨占線程,同時將state的值通過CAS操作設置成1,如果成功的話,直接返回true。表示加鎖成功。這個時候會執行這個if判斷中代碼。執行setHead(node),這個時候AQS隊列如下圖:
if (p == head && tryAcquire(arg)) {
setHead(node); // node就是addWaiter的尾巴節點,
p.next = null; // help GC 看Java四種引用就知道 前面那個節點的next設置為null
failed = false; // 局部變量failed初始為true,要下去執行cancelAcquire,這里設置為false,不執行cancelAcquire了
return interrupted; // false
}
private void setHead(Node node) {
head = node; // 類變量head指向addWaiter的尾巴節點
node.thread = null; // 這個節點thread=null
node.prev = null; // 這個節點prev==null 因為要變成頭結點,非循環雙向鏈表,所以前驅指針為null
}
復制代碼
這個時候原來的線程B節點出隊列(因為B節點要去執行了),然后永遠會維護一個頭結點中thread為null的AQS隊列。
2.2.3 lock方法中有三個線程的情況
lock方法中有三個線程情況,如下圖:
三個線程和兩個線程的情況是差不多的,即加鎖成功的節點永遠是頭結點的下一個節點中的線程加鎖成功,因為是公平鎖。
2.3 非公平鎖加鎖不需要隊列:NonfairSync類的lock()方法(了解即可,搞懂了公平鎖的lock(),這個就好懂了,然后只要知道公平鎖lock()和非公平鎖lock()區別就好)
非公平鎖加鎖流程:
- 非公平鎖會走來直接嘗試加鎖;
- 如果加鎖成功,直接執行線程中的代碼;
- 如果加鎖不成功,直接走公平鎖的邏輯。
2.4 其他加鎖方法:ReentrantLock類的tryLock()方法(了解即可,和上面公平鎖lock()大同小異)
tryLock()方法和lock()方法是差不多,tryLock方法,嘗試加鎖不成功后就直接返回false,具體的代碼如下:
public boolean tryLock() {
return sync.nonfairTryAcquire(1);
}
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
復制代碼
2.5 其他加鎖方法:ReentrantLock類的tryLock(long timeout, TimeUnit unit)方法(了解即可,和上面公平鎖lock()大同小異)
-
tryLock(long timeout, TimeUnit unit)方法,加了一個獲取鎖的時間,如果這個時間內沒有獲取到鎖,直接返回false,表示加鎖失敗;如果在這個時間內調用tryAcquire(arg)獲得到鎖,表示加鎖成功,tryAcquireNanos(int arg, long nanosTimeout)方法返回值直接為true,即tryLock(long timeout, TimeUnit unit)方法返回值為true。
-
如果tryAcquire(arg)返回為false,會執行doAcquireNanos(arg, nanosTimeout)方法,走來先將當前節點用尾插法的方式插入到AQS隊列中去,如果AQS隊列沒有初始化,直接初始化,將當前的節點放入到尾結點中去。然后進入死循環,這個時候判斷當前節點的上一個節點是不是頭結點,再次嘗試加鎖,如果成功直接返回true,如果失敗將當前的節點的線程直接park指定的時間,當時間到了直接喚醒。再次嘗試獲取鎖,如果成功直接返回true,如果失敗直接返回false,這個方法中是可以直接響應中斷的。
詳細:
public boolean tryLock(long timeout, TimeUnit unit)
throws InterruptedException {
return sync.tryAcquireNanos(1, unit.toNanos(timeout));
}
public final boolean tryAcquireNanos(int arg, long nanosTimeout)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException(); // 這里會立即響應中斷
return tryAcquire(arg) ||
doAcquireNanos(arg, nanosTimeout);
}
private boolean doAcquireNanos(int arg, long nanosTimeout)
throws InterruptedException {
if (nanosTimeout <= 0L)
return false;
final long deadline = System.nanoTime() + nanosTimeout;
final Node node = addWaiter(Node.EXCLUSIVE);
boolean failed = true;
try {
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return true;
}
nanosTimeout = deadline - System.nanoTime();
if (nanosTimeout <= 0L)
return false;
if (shouldParkAfterFailedAcquire(p, node) &&
nanosTimeout > spinForTimeoutThreshold)
LockSupport.parkNanos(this, nanosTimeout); // park指定的時間
if (Thread.interrupted())
throw new InterruptedException(); // 有中斷立即響應
}
} finally {
if (failed)
cancelAcquire(node);
}
}
復制代碼
2.6 其他加鎖方法:ReentrantLock類的lockInterruptibly()方法(了解即可,和上面公平鎖lock()大同小異)
lockInterruptibly和lock的區別:
- lockInterruptibly是會立即響應中斷的:並且在park中的線程也會interruptibly喚醒的,因為這個時候返回true,直接拋出異常,響應對應的中斷。
- lock是要等線程執行完才會響應中斷:是因為park中線程被中斷喚醒后,沒有拋出異常,只是將中斷的標志設置成了true,等到獲取到鎖,執行完,才會響應對象的中斷。
lockInterruptibly是會立即響應中斷的(源碼解釋)
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
public final void acquireInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
if (!tryAcquire(arg))
doAcquireInterruptibly(arg);
}
private void doAcquireInterruptibly(int arg)
throws InterruptedException {
final Node node = addWaiter(Node.EXCLUSIVE);
boolean failed = true;
try {
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
throw new InterruptedException(); // 有中斷立即響應
}
} finally {
if (failed)
cancelAcquire(node);
}
}
復制代碼
lock是要等線程執行完才會響應中斷(源碼解釋)
final void lock() {
acquire(1);
}
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true; // 沒有立即響應中斷,僅僅設置一個標志位interrupt=true
}
} finally {
if (failed)
cancelAcquire(node);
}
}
復制代碼
三、AQS源碼對於lock.unlock()的實現
講完了加鎖的過程,我們再來看看解鎖的過程,即ReentrantLock類的unlock()方法。
3.1 解鎖整體流程圖
3.2 解鎖涉及的函數:unlock()->release()->tryRelease() + 解鎖三種情況
解鎖涉及的函數:unlock()->release()->tryRelease()
public void unlock() {
sync.release(1);
}
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
復制代碼
線程解鎖的三種情況:
-
當前線程不在AQS隊列中,執行tryRelease()方法。
如果當前線程不是重入鎖(即滿足 if(c==0)為true),直接將當前的線程獨占標識去除掉,然后將state的值通過CAS的操作改成0;
如果當前線程加的是重入鎖(即滿足 if(c!=0)為false),解鎖一次,state的值減1,如果state的值是等於0的時候,返回true。表示解鎖成功。 -
AQS隊列中只有一個頭結點,這個時候tryRelease()返回的結果和上面的情況是一樣的。這個時候返回的true,會進當前的if中去,然后判斷頭結點是不是為null和頭結點中waitStatus的值是不是等於0。這個時候head不等於null,但是waitState是等於0,if判斷不成立,不會執行unpark的方法。會直接返回true。表示解鎖成功。
-
AQS隊列中不止一個頭結點,還有其他節點,這個時候tryRelease()返回的結果和上面的情況是一樣的。這個時候返回的true,會進當前的if中去,然后判斷頭結點是不是為null和頭結點中waitStatus的值是不是等於0。這個時候head不等於null,但是waitState是等於-1,if判斷成立,會執行unpark的方法。unpark方法中會unpark頭結點的下一個節點,然后如果當前的節點的狀態是取消的狀態,會從最后一個節點開始找,找到當前節點的下一個不是取消狀態的節點進行unpark。這個時候也會直接返回true。表示解鎖成功。
3.3 重要函數:unparkSuccessor()(在release()中被調用)
private void unparkSuccessor(Node node) { // 同步隊列中的頭結點head傳遞過來
int ws = node.waitStatus; // 為 -1
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0); // 設置狀態 node的ws變為0
Node s = node.next; // 找到工作隊列的head的后面一個節點
if (s == null || s.waitStatus > 0) { // head后面這個節點為空,或者waitStatus大於0
s = null; // 如果是因為waitStatus大於0而進入這個if,設置head后面的這個節點為null
for (Node t = tail; t != null && t != node; t = t.prev) // 從尾巴開始遍歷,布局變量為t 沒有遍歷完或沒找就繼續找
if (t.waitStatus <= 0) // 如果t.waitStatus <= 0,將這個t記錄到s,后面unpark用 找到當前節點的下一個不是取消狀態的節點進行unpark
s = t;
}
if (s != null) // head后面的節點不為空,直接對head后面這個節點unpark 畢竟公平鎖
LockSupport.unpark(s.thread);
}
復制代碼
問題:第二個if是如何判斷三情況的 if (h != null && h.waitStatus != 0) ?
回答:
- 第一種情況,沒有AQS隊列,head一定為null,就是return true;
- 第二種情況,有AQS隊列但是同步隊列中只有一個節點,head不為null,但是唯一的一個節點既是頭結點也是尾節點,所有h.waitStatus == 0 ,不滿足條件,就是return true;
- 第三種情況,有AQS隊列但是同步隊列中超過一個節點,head不為null,頭結點和尾節點是不同節點,所以h.waitStatus==-1,滿足第二層if判斷,就是 unparkSuccessor(h); 然后 return true;
小結:雖然都是返回為true,解鎖成功,但是內部邏輯是不同的。
兩種重要問題:
- 問題:為什么需要同步隊列AQS?
回答:同步隊列是lock公平鎖的內部數據結構,非公平鎖不需要同步隊列。 - 問題:為什么同步隊列是非循環雙鏈表,非循環單鏈表不行嗎?
回答:公平鎖中,lock.lock()使用尾插法插入,但是,在調用lock.unlock()方法的時候,由於頭節點是成功獲取到同步狀態的節點,而頭節點的線程釋放了同步狀態后,將會喚醒其他后續節點s,后繼節點的線程被喚醒后需要檢查自己的前驅節點是否是頭節點,如果是則嘗試獲取同步狀態。所以為了能讓后繼節點獲取到其前驅節點,同步隊列便設置為雙向鏈表,而等待隊列沒有這樣的需求,就為單鏈表。
四、面試金手指(ReentrantLock中lock()與unlock(),即AQS加鎖解鎖)
面試問題:lock機制是如何實現公平鎖的加鎖和解鎖的(因為synchronized無法實現公平鎖)
回答:下面4.1 4.2 4.3
4.1 先搞懂AQS類中的基本元素Node節點
問題:AQS內部類Node
回答:AQS本質是一個非循環的雙向鏈表(也可以稱為隊列),所以它是由一個個節點構成的,就是Node,后面的lock() unlock() await() signal()/signalAll()都是以Node為基本元素操作的。
問題:AQS類中的Node內部類中需要保存什么信息呢?
回答:一個六個,其中,prev、next 兩個Node類型,表示做指針,thread 存放節點的值,因為AQS隊列的節點就是存放線程的,所以這個值類型就是Thread,最后,nextWaiter也是Node類型,表示下一個等待節點, waitStatus表示當前節點等待狀態,SHARED|EXCLUSIVE 表示是獨占還是共享。
volatile int waitStatus; //當前節點等待狀態
volatile Node prev; //上一個節點
volatile Node next; //下一個節點
volatile Thread thread; //節點中的值
Node nextWaiter; //下一個等待節點
//指示節點共享還是獨占,默認初始是共享
static final Node SHARED = new Node();
static final Node EXCLUSIVE = null;
復制代碼
記住一個Node節點的六個屬性(共享/獨占算一個),下面看源碼就輕松些
- 處在同步隊列中使用到的屬性(本文用:加鎖、解鎖)包括:next prev thread waitStatus,所以同步隊列是雙向非循環鏈表,涉及的類變量AbstractQueuedSynchronizer類中的head和tail,分別指向同步隊列中的頭結點和尾節點。
- 處在等待隊列中使用到的屬性(下一篇博文用:阻塞、喚醒)包括:nextWaiter thread waitStatus,所以等待隊列是單向非循環鏈表,涉及的類變量ConditionObject類中的firstWaiter和lastWaiter,分別指向等待隊列中的頭結點和尾節點。
- AQS隊列是工作隊列、同步隊列,是非循環雙向隊列:當使用到head tail的時候,就說AQS隊列建立起來了,單個線程不使用到head tail,所以AQS隊列沒有建立起來;
- 等待隊列,是非循環單向隊列:當使用firstWaiter lastWaiter的時候,就說等待隊列建立起來了。
- lock()和unlock()就是操作同步隊列:lock()將線程封裝到節點里面(此時,節點使用到的屬性是thread nextWaiter waitStatus),放到同步隊列,即AQS隊列中,unlock()將存放線程的節點從同步隊列中拿出來,表示這個線程工作完成。
- await()和signal()就是操作等待隊列:await()將線程封裝到節點里面(此時,節點使用到的屬性是thread prev next waitStatus),放到等待隊列里面,signal()從等待隊列中拿出元素。
問題:為什么負責同步隊列的head和tail在AbstractQueuedSynchronizer類中,但是負責等待隊列的firstWaiter和lastWaiter在ConditionObject類中?
回答:
- 線程同步互斥是直接通過ReentrantLock類對象 lock.lock() lock.unlock()實現的,而ReentrantLock類對象是調用AQS類實現加鎖解鎖的,所以負責同步隊列的head和tail在AbstractQueuedSynchronizer類中;
- 線程阻塞和喚醒是通過ReentrantLock類對象lock.newCondition()得到一個對應,condition引用指向這個對象,然后condition.await() condition.signal()實現的,所以負責等待隊列的firstWaiter和lastWaiter在ConditionObject類中。
4.2 公平鎖加鎖
4.2.1 單線程加鎖過程
4.2.1.1 整體路程圖(第1次加鎖 + 第2~n次加鎖)
公平鎖加鎖流程(只有一個線程的時候):
- 直接調用tryAcquire,然后判斷state的是不是等於0
- 對於A線程第1次加鎖,等於0,證明是第一次加鎖,第一次加鎖hasQueuedPredecessors()一定通過(返回為false取反為true),通過CAS操作將state的值改成1,然后true,返回true表示加鎖成功,就完成了加鎖。
- 對於A線程第2~n次加鎖,不等於0,表示不是第一次加鎖,這個鎖是重入鎖,這個時候將原來的state值繼續通過CAS操作加上1,再次返回true,表示加鎖成功,就完成了加鎖。
tip1:需要注意的是,這個時候AQS的隊列沒有創建出來。
tip2:setExclusiveOwnerThread(current); // 這里是設置當前節點為獨占
tip3:看源碼的時候,知道自己在看什么,這里是看FairSync的lock()方法實現
tip4:源碼一般命名優美,可以從命名上來看,幫助理清思路,例如 lock()是加鎖、acquire()是去獲得tryAcquire() 是嘗試加鎖、acquireQueued()是獲得隊列
4.2.1.2 重要方法:tryAcquire()源碼解析
對於只有一個線程A使用lock.lock();加鎖,最重要的方法就是tryAcquire,就是這個方法,將線程A區分對待,第一次加鎖和非第一次加鎖,源碼拿出來講一講:
protected final boolean tryAcquire(int acquires) { // **1、tryAcquire是去獲取,2、返回為true就是使用獲取的方式加鎖成功(可以第一次,也可以是重入鎖)**
final Thread current = Thread.currentThread();
int c = getState(); // 當前狀態
if (c == 0) { // 當前狀態為0,就是默認狀態
if (!hasQueuedPredecessors() && // **1、hasQueuedPredecessors這個方法重要,下面解釋**
compareAndSetState(0, acquires)) { // **1、只要上面那個hasQueuedPredecessors()返回為false,取反為true,這個cas一定是可以通過的,只是自旋等一下罷了**
setExclusiveOwnerThread(current); // **1、設置當前線程為獨占線程,因為當前線程已經加鎖成功了,所以設置當前線程為互斥資源的獨占線程**
//**2、為什么說當前線程加鎖成功了,因為這里返回true啊**
return true;
}
}
else if (current == getExclusiveOwnerThread()) { // 這句表示當前線程為獨占線程
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true; // **1、因為當前線程是獨占線程,所以一定是加鎖成功,這里返回true就好
// 2、既然已經是獨占線程,就沒有必要再次設置當前線程為獨占線程了,直接返回true**
}
return false; // **1、如果既不是第一次,也不是重入鎖,就不能通過獲取的方式去加鎖,要自己加鎖,這里返回false,加鎖失敗**
}
復制代碼
4.2.1.3 重要方法:hasQueuedPredecessors()源碼解析
問題:hasQueuedPredecessors()源碼解析?
回答:
-
如果h == t成立,h和t均為null或是同一個具體的節點,無后繼節點,方法返回false,表示要通過獲取鎖tryLock()加鎖,這種情況是第一個節點,類變量tail == head == null。
key:如果類變量head==tail,表示沒有節點或只有一個節點,所以一定是沒有前驅節點的,方法直接返回false,不用多說,注意,后面的,head!=tail至少兩個節點 -
如果h!=t成立(至少兩個節點),而且 head.next == 為null,方法返回true。什么情況下h!=t的同時h.next==null??有其他線程第一次正在入隊時,可能會出現。見AQS的enq方法,compareAndSetHead(node)完成,還沒執行tail=head語句時,此時tail=null,head=newNode,head.next=null。
key:此時,頭結點設置為新建節點,所以head=newNode
但是,還未將頭結點設置為尾節點,所以tail=null,為默認值
同時,這是第一次執行enq()方法,沒有設置 node.prev = t; 和 t.next = node;,所以head.next=null。 -
如果h!=t成立(當前類變量head 和 tail不是同一個,至少兩個節點),head.next != null,(true && (false || ))則判斷head.next是否是當前線程,如果是當前線程(true && (false || false)),方法返回false,key:表示要通過獲取鎖的方式加鎖;如果不是當前線程(true && (false || true))返回true,key:表示正在運行的線程不是當前的這個current,需要等待。
(head節點是獲取到鎖的節點,但是任意時刻head節點可能占用着鎖,也可能釋放了鎖(unlock()),未被阻塞的head.next節點對應的線程在任意時刻都是有必要去嘗試獲取鎖)
實際上,hasQueuedPredecessors返回為true不通過,只需要等一段時間罷了(上面關於hasQueuedPredecessors方法的意義:如果該方法返回true,則表示有線程比當前線程更早地請求獲取鎖,因此需要等待前驅線程獲取並釋放鎖之后才能繼續獲取鎖)
問題:hasQueuedPredecessors()方法是用來干什么的?
回答:
- hasQueuedPredecessors()方法的意義:該方法表示對加入了同步隊列中當前節點是否有前驅節點的判斷,如果該方法返回true,則表示有線程比當前線程更早地請求獲取鎖,因此需要等待前驅線程獲取並釋放鎖之后才能繼續獲取鎖,因為是公平鎖,要按照隊列來。
- hasQueuedPredecessors()方法的調用以及返回的意義:hasQueuedPredecessors()方法只在tryAcquire()方法里面被調用執行過,hasQueuedPredecessors()返回false表示要嘗試通過獲取鎖的方式加鎖,沒有前驅節點請求獲取鎖,返回為true表示不需要通過獲取鎖的方式加鎖,已經有前驅節點請求獲取了。
問題:如何進入hasQueuedPredecessors()方法?
回答:lock() -> acquire() -> tryAcquire() -> hasQueuedPredecessors()
4.2.2 兩個線程加鎖過程(A線程 B線程)
4.2.2.1 第一種情況:當線程B進來的時候,死循環中線程A沒有解鎖
第一,建立AQS隊列
我們假設線程A直接獲取到了鎖(獲取鎖的過程和上面單線程一樣,不再贅言),但是線程A還沒有解鎖,這個時候線程B來進行加鎖,走來會執行tryAcquire()方法,這個時候線程A沒有解鎖,所以這個tryAcquire()方法會直接返回false(state!=0,也不是重入鎖),然后會調用addWaiter(Node.EXCLUSIVE)方法(addWaiter()是新方法:上面一個線程的時候沒有涉及到,這里要重點分析),這個時候會在這個方法中的enq(node)的方法中初始化AQS隊列,也會利用尾插法將當前的節點插入到AQS隊列中去。AQS隊列如下圖所示:
對於當前的AQS隊列解釋:
- 這時AQS隊列新建起來了
- head節點中thread=null表示沒有存放任何線程,waitStatus=0表示什么都不是
- tail節點中thread=線程B表示存放的是線程B,waitStatus=0表示什么都不是
第二,addWaiter()方法中調用enq()方法,新建AQS隊列
完成AQS隊列的方法是addWaiter()中調用的enq()方法,且看addWaiter()方法和enq()方法
private Node addWaiter(Node mode) { // **1、實際參數是Node.EXCLUSIVE,就是當前獨占節點,表示下一個等待節點就是正在獨占的那個線程的節點,因為它釋放鎖就要到插入了,所以這個方法稱為addWaiter,意為添加下一個等待節點**
Node node = new Node(Thread.currentThread(), mode); // 新建一個節點,存放當前線程,當前線程為內容,實參為下一個等待節點nextWaiter
Node pred = tail; // 當前尾節點賦值,當前tail==null
if (pred != null) {
node.prev = pred; // 如果不為空,進來,新建節點的前一個是之前的尾節點,就是尾插法
if (compareAndSetTail(pred, node)) { // 設置新的尾節點,從之前的尾節點pred到現在的node
pred.next = node; // 之前尾節點的next設置為這個節點
// **由此可知,尾插法三步驟:設置新節點的prev為之前尾節點、重新設置tail類變量的指向、設置之前尾節點的next為新建節點(就是三個Node類型指針而已,很簡單)**
return node; // 返回新建節點
}
}
enq(node); // 返回值沒有接收者,但是隊列新建好了
return node; // 返回這個新建的節點
}
private Node enq(final Node node) {
for ( ; ; ) { // **1、死循環,不創建好AQS隊列不退出**
Node t = tail;
if (t == null) { // Must initialize **1、第一次進入,必須初始化,這里表示連尾節點都沒有**
if (compareAndSetHead(new Node())) // **for+if(cas)就是線程同步**
tail = head; // **1、新建一個節點,設置為頭結點,因為只有一個節點,所以尾節點也是這個節點**
} else {
node.prev = t; // **1、這是時候,第二次循環,因為head tail都是新節點,第二次循環中使用 Node t = tail;將t設置為這個新節點**
if (compareAndSetTail(t, node)) { // 方法名是compareAndSetTail,表示設置尾節點,自旋,知道設置成功 for+if(cas)就是線程同步,設置tail類變量,將tail從t變為node,所以傳入參數node是尾節點
t.next = node; // 尾節點指向參數node,頭結點還是指向t
// **由此可知,尾插法三步驟:設置參數節點的prev為之前尾節點t、重新設置tail類變量的指向從之前的t到參數節點node、設置之前尾節點t的next為參數節點node(就是三個Node類型指針而已,很簡單),最后隊列兩個元素 t 和 node**
return t; // 返回頭結點t
}
}
}
}
復制代碼
tip:head和tail是類變量,類似指針,指向其他節點
compareAndSetTail(t, node) // 設置tail類變量,將tail從t變為node,所以傳入參數node是尾節點
compareAndSetHead(t, node) // 設置tail類變量,將head從t變為node,所以傳入參數node是頭節點
compareAndSetState(0, acquires) // 設置state類變量,從0到1,cas保證安全
第三,acquireQueued()方法接收addWaiter()返回值,調用shouldParkAfterFailedAcquire()方法,修改AQS隊列中節點的waitStatus值從0到1
方法addWaiter()返回當前的節點,然后調用acquireQueued(addWaiter(Node.EXCLUSIVE), arg))方法(tip:返回的剛剛在addWaiter()方法中新建的最尾巴的節點作為acquireQueued方法的參數,arg參數是1,傳遞過來的)。這個方法中是一個死循環,由於線程A沒有釋放鎖(tryAcquire()方法會直接返回false(state!=0,也不是重入鎖)),會執行shouldParkAfterFailedAcquire(p, node)(p表示線程B節點的上一個節點,p = node.predecessor();就是最尾巴節點上一個,node表示線程B的節點,就是addWaiter()方法中新建的最尾巴的節點)第一次進這個方法會將線程B節點的上一個節點的waitStatus的值改成-1(執行最后一個else compareAndSetWaitStatus(pred, ws, Node.SIGNAL);),然后返回false,這個時候的AQS隊列如下圖:
tip:仔細對比兩個圖,和上一次的改變是:線程B節點的上一個節點的waitStatus的值從0修改成-1。
-
第一次進入shouldParkAfterFailedAcquire()方法會將線程B節點的上一個節點的waitStatus的值改成-1,然后返回false。此時的AQS工作隊列:head節點thread==null表示沒有存放線程,waitStatus=-1表示后面節點的線程需要釋放;tail節點thread=線程B表示存放的是線程B,waitStatus=0表示什么都不是。
-
第二次進入shouldParkAfterFailedAcquire()方法的時候,會返回true( if (ws == Node.SIGNAL) return true;),會執行后面的方法parkAndCheckInterrupt()( LockSupport.park(this);),這個時候線程B就會被park在這。(1、直到線程A解鎖了,第二種情況可以當做第一個情況后面的執行來看)
4.2.2.2 第二種情況:當線程B進來的時候,死循環中線程A已經解鎖
上面的情況都是在線程A沒有解鎖的時候,如果在死循環中線程A已經解鎖了。這個時候判斷線程B節點的上一個節點是不是頭結點,如果是的話,直接執行tryAcquire(),將當前線程B設置成獨占線程,同時將state的值通過CAS操作設置成1,如果成功的話,直接返回true。表示加鎖成功。這個時候會執行這個if判斷中代碼。執行setHead(node),這個時候AQS隊列如下圖:
if (p == head && tryAcquire(arg)) {
setHead(node); // node就是addWaiter的尾巴節點,
p.next = null; // help GC 看Java四種引用就知道 前面那個節點的next設置為null
failed = false; // 局部變量failed初始為true,要下去執行cancelAcquire,這里設置為false,不執行cancelAcquire了
return interrupted; // false
}
private void setHead(Node node) {
head = node; // 類變量head指向addWaiter的尾巴節點
node.thread = null; // 這個節點thread=null
node.prev = null; // 這個節點prev==null 因為要變成頭結點,非循環雙向鏈表,所以前驅指針為null
}
復制代碼
這個時候原來的線程B節點出隊列(因為B節點要去執行了),然后永遠會維護一個頭結點中thread為null的AQS隊列。
4.3 lock.unlock()解鎖過程
線程解鎖的三種情況:
-
當前線程不在AQS隊列中,執行tryRelease()方法。
如果當前線程不是重入鎖(即滿足 if(c==0)為true),直接將當前的線程獨占標識去除掉,然后將state的值通過CAS的操作改成0;
如果當前線程加的是重入鎖(即滿足 if(c!=0)為false),解鎖一次,state的值減1,如果state的值是等於0的時候,返回true。表示解鎖成功。 -
AQS隊列中只有一個頭結點,這個時候tryRelease()返回的結果和上面的情況是一樣的。這個時候返回的true,會進當前的if中去,然后判斷頭結點是不是為null和頭結點中waitStatus的值是不是等於0。這個時候head不等於null,但是waitState是等於0,if判斷不成立,不會執行unpark的方法。會直接返回true。表示解鎖成功。
-
AQS隊列中不止一個頭結點,還有其他節點,這個時候tryRelease()返回的結果和上面的情況是一樣的。這個時候返回的true,會進當前的if中去,然后判斷頭結點是不是為null和頭結點中waitStatus的值是不是等於0。這個時候head不等於null,但是waitState是等於-1,if判斷成立,會執行unpark的方法。unpark方法中會unpark頭結點的下一個節點,然后如果當前的節點的狀態是取消的狀態,會從最后一個節點開始找,找到當前節點的下一個不是取消狀態的節點進行unpark。這個時候也會直接返回true。表示解鎖成功。
問題:第二個if是如何判斷三情況的 if (h != null && h.waitStatus != 0) ?
回答:
- 第一種情況,沒有AQS隊列,head一定為null,就是return true;
- 第二種情況,有AQS隊列但是同步隊列中只有一個節點,head不為null,但是唯一的一個節點既是頭結點也是尾節點,所有h.waitStatus == 0 ,不滿足條件,就是return true;
- 第三種情況,有AQS隊列但是同步隊列中超過一個節點,head不為null,頭結點和尾節點是不同節點,所以h.waitStatus==-1,滿足第二層if判斷,就是 unparkSuccessor(h); 然后 return true;
小結:雖然都是返回為true,解鎖成功,但是內部邏輯是不同的。
兩種重要問題:
- 問題:為什么需要同步隊列AQS?
回答:同步隊列是lock公平鎖的內部數據結構,非公平鎖不需要同步隊列。 - 問題:為什么同步隊列是非循環雙鏈表,非循環單鏈表不行嗎?
回答:公平鎖中,lock.lock()使用尾插法插入,但是,在調用lock.unlock()方法的時候,由於頭節點是成功獲取到同步狀態的節點,而頭節點的線程釋放了同步狀態后,將會喚醒其他后續節點s,后繼節點的線程被喚醒后需要檢查自己的前驅節點是否是頭節點,如果是則嘗試獲取同步狀態。所以為了能讓后繼節點獲取到其前驅節點,同步隊列便設置為雙向鏈表,而等待隊列沒有這樣的需求,就為單鏈表。
五、小結
ReentrantLock中lock()、unlock() 全解析,完成了。
天天打碼,天天進步!!!
作者:毛毛的學習筆記
鏈接:https://juejin.im/post/6893438433826439182
來源:掘金
著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。