文中都是自己總結的,如果哪裏邏輯不對或者寫的不清楚的還請評論區中指出。
前言:本篇主要基於源碼來解讀ReetrantLock加鎖和解鎖的過程,reetrantLock的主體思想就是通過對鎖status的加減操作來實現的,如果當前線程獲得當前鎖就把status+1,再次獲取就繼續+1,釋放鎖就是-1; 如果加鎖的時候發現鎖status不等於0就把當下線程放入到一個FIFO隊列(就是一個雙向鏈表)中並掛起,被放入等待隊列的線程等待正在執行的線程喚醒它或者等待的線程被中斷
當然了其中運用了線程自旋和cas以及volatile關鍵字來保證線程能夠正確的獲取鎖和釋放鎖。
文中標註解的地方都是對理解整體流程沒有影響的代碼,這部分代碼可以後續在看
一 .ReetrantLock 非公平鎖講解
1.非公平鎖初始化,代碼塊1
//默認構造器,加載的爲非公平鎖
Public ReentrantLock(){
sync=new NonfairSync();
}
2.非公平鎖加鎖過程lock.lock()
非公平鎖在獲取鎖時,直接去獲取掙錢鎖,而不管等待隊列有沒有等待的線程,這樣儘可能的減少了加入等待隊列及線程被掛起的時間,代碼講解如下,代碼塊2
// 獲取鎖
//status 需要說明下:status是一個鎖的全局變量,加鎖釋放鎖、或者重入鎖(status多次加1)都是通過通過cas來原子的對status加1減1 來表示的。
final void lock() {
//使用cas 嘗試 把status 狀態設置成1 設置成1 就表表獲取鎖了,要是失敗說明已經被佔用了
if (compareAndSetState(0, 1)) //註解2
//設置爲當前線程佔有鎖
setExclusiveOwnerThread(Thread.currentThread());//註解3
//如果stateOffset 狀態設置爲1失敗說明,有可能是當前線程再次獲取鎖,或者別的線程佔有鎖
else
acquire(1);
}
//繼承自aqs,再次嘗試獲取鎖tryAcquire(重入鎖 或者在這期間上個線程已經釋放了鎖) 如果獲取了鎖那麼就返回true,獲取鎖失敗的話就把該線程加入到等待隊列
//addWaiter 用於把當前節點加入到等待隊列中,通過cas 和自旋保證一定會加入成功
//acquireQueued:如果當前線程爲首節點就自旋獲取鎖,否則掛起 ;直到被喚醒並獲取鎖,然後返回當前線程的中斷位狀態
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
//acquireQueued該方法返回true說明,該線程是被中斷喚醒 並獲取的鎖,所以需要重新設置下中斷位
//selfInterrupt這裏看不懂沒關係,繼續往下梳理加鎖的主流程即可,回頭再看
selfInterrupt();//調用 Thread.currentThread.interrupt() 把當然線程中斷位 設成true,
}
當我們調用lock.lock()獲取非公平鎖的時候,首先會先通過cas嘗試獲取鎖,如果成功的搶佔了鎖,就省略了加入等待隊列步驟。
如果獲取鎖失敗的話,有可能持有鎖的線程爲當前線程,調用tryAcquire再次嘗試獲取鎖。代碼塊3
//嘗試獲取鎖,
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
//獲取當然鎖的狀態
int c = getState();
//如果鎖狀態爲0說明在這期間上個線程已經釋放鎖
if (c == 0) {
//通過cas 設置鎖狀態,如果設置成功就把鎖設爲當前線程獨有
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
//如果鎖狀態不等於0,則判斷該鎖是否爲當前線程擁有(鎖重入),如果是則把鎖狀態+1
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
//鎖狀態是個int類型,這裏應該是擔心鎖重複的次數太多然後 溢出變成負數
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
//鎖狀態更新
setState(nextc);
return true;
}
return false;
}
再次嘗試獲取鎖,首先判斷status是否等於0,防止上個線程已經釋放鎖。如果沒有釋放鎖,判斷持有鎖的線程是否爲當前線程,如果持有鎖的線程等於當前線程,那麼當前鎖爲衝入鎖,status再次+1
上述操作都失敗的話,把當前線程加入到等待隊列中。等待隊列爲一個FIFO隊列,就是一個雙向鏈表,ReetrantLock (繼承來的)中存在了一個頭結點指針 head 和一個尾節點指針tail(node節點的數據結構代碼在註解1)
我們來看下線程加入鎖等待隊列的代碼,如下:代碼塊4
//把當前線程加入到等待隊列中去,等待隊列爲一個雙向列表
private Node addWaiter(Node mode) {
//給當前線程創建一個node節點,見註解1,mode傳入的爲null
Node node = new Node(Thread.currentThread(), mode);
//等待隊列的尾節點指針
Node pred = tail;
//如果尾節點不等於null 說明有別的線程在等待該鎖
if (pred != null) {
//把當前節點指向上一節點的指針指向尾節點原來指向的那個節點,就是把當前節點加入到尾節點
node.prev = pred;
//通過cas把尾節點 指向爲當前結點
//成功 ,則說明這期間沒有別的線程加入到等待隊列
//失敗,則說明在這期間有別的線程已經加入到了尾節點
if (compareAndSetTail(pred, node)) {
//上一個節點指向當前結點
pred.next = node;
return node;
}
}
enq(node);
return node;
}
這裏樂觀的認爲可以直接加入到等待隊列,如果發現併發或者該節點是第一次加入,調用enq方法初始化等待隊列並通過cas及自旋保證成功加入到等待隊列隊尾。如下 代碼塊5
//第一種情況:第一次加入等待隊列尾節點爲null
//第二種情況:第n次加入成功把當前線程節點加入到等待隊列
//第三種情況:第n加入,加入到等待隊列尾部的時候,有別的線程已經加入到等待隊列了
private Node enq(final Node node) {
//防止併發加入到等待隊列的尾節點,
for (;;) {
Node t = tail;
//尾節點爲null,說明是第一次加入,需要初始化頭尾節點
if (t == null) { // Must initialize
if (compareAndSetHead(new Node()))
tail = head;
} else {
//自旋直到當前結點加入到等待隊列爲止
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
加入隊列的時候通過enq方法來保證,當前線程節點一定成功的加入到等待隊列,加入到等待隊列之後呢?
我們來看下加入等待隊列之後的acquireQueued操作:代碼塊6
//如果發現當前節點爲首節點就自旋爭搶鎖,並返回當前線程的中斷狀態,用於後續操作判斷當前線程的是怎麼被喚醒的
//如果該節點還有前置節點就把當前線程掛起等待被喚醒
//被喚醒後,如果當前線程節點變爲首節點就爭搶鎖,否則繼續上兩個步驟
//被喚醒且獲取鎖後,返回當前線程的中斷狀態(當且只有被掛起後纔會可能返回true)
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
//如果該節點是頭節點的話 自旋爭搶鎖,
for (;;) {
//獲取當前結點的前置節點
final Node p = node.predecessor();
//如果前置節點爲head說明,他當前線程是等待隊列中的第一個,那麼就嘗試獲取鎖
//獲取鎖成功就重置head節點,並返回false
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
//普通鎖的情況下:shouldParkAfterFailedAcquire爲第一次加入等待隊列的節點設置節點等待狀態(waitStatus =-1),然後返回false繼續自旋 嘗試獲取鎖
//shouldParkAfterFailedAcquire 只有發現當前節點不是首節點纔會返回true ,然後掛起當前線程,
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
//如果該線程是被中斷喚醒的,用於輔助後續操作判斷當前線程是被中斷喚醒的
interrupted = true;
}
} finally {
//如果該方法因爲某些特殊情況意外的退出(沒有獲取鎖就退出了),那麼就取消嘗試獲取鎖
//設置節點狀態爲CANCELLED
if (failed)
cancelAcquire(node);
}
}
//只有 當該節點的前置節點爲普通的鎖時(沒有使用condition)返回true
//普通的鎖第一次加入等待隊列時把waitStatus 置爲-1
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
//如果當前節點不爲首節點的話 直接返回true
if (ws == Node.SIGNAL)
return true;
//pred狀態爲CANCELLED,則一直往隊列頭部回溯直到找到一個狀態不爲CANCELLED的結點,將當前節點node掛在這個結點的後面。
//當前獲取鎖的時候發生某些不可知的錯誤就把,等待隊列中的節點設成CANCELLED(1)狀態
if (ws > 0) {
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
//沒有condition 的情況下,且是第一個加入到等待列表
//如果在這期間頭結點沒有邊的話,就把當前節點waitStatus設置爲-1狀態,
//node的狀態爲-1,說明當前節點的鎖是一個普通的鎖
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
//掛起當前線程,並返回當前線程的中斷狀態(用於後續操作來判斷該線程是怎麼被喚醒的?),
private final boolean parkAndCheckInterrupt() {
//掛起當前線程
//park被喚醒的條件有 1.線程調用了unpark,2:其它線程中斷了線程;3:發生了不可預料的事情
LockSupport.park(this);
//返回當前線程的中斷位狀態,但是會清除中斷位
return Thread.interrupted();
}
通過查看acquireQueued源碼我們知道,當我們把當前線程節點加入到等待隊列中後,如果該節點爲首節點當前線程就自旋爭搶鎖,否則就調用LockSupport.park掛起當前線程,等待被調用uppark或者中斷喚醒。只有被中斷喚醒的併成功搶佔鎖的線程,acquireQueued方法纔會返回true。
因爲LockSupport.park掛起的線程不僅會被LockSupport.unpark方法喚醒,還會被中斷喚醒,所以線程被喚醒後調用了下Thread.interrupted()方法來返回下當前線程的中斷狀態,但是該方法會清楚掉線程的中斷狀態,所以在代碼塊2又一次的設置了下中斷狀態。這也是ReentrantLock區別於synchronized關鍵字的一個地方。
ReentrantLock支持等待隊列中的線程中斷,synchronized不支持。
3.非公平鎖釋放鎖的過程
lock.unlock();
public void unlock() {
sync.release(1);
}
public final boolean release(int arg) {
//tryRelease嘗試釋放鎖(鎖status-1),如果當前線程沒有佔有的鎖(鎖status=0) 返回true
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;
//如果當前鎖狀態爲0,說明已經釋放掉鎖,並把當前鎖持有線程設爲null
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
3.lock.lockInterruptibly() 簡述
這裏不就貼源碼了,過程和普通加鎖很類似,唯一的區別在於lock.lockInterruptibly()方法遇到中斷直接拋出中斷異常,所在在使用該方法的時候需要處理下中斷異常
二 .ReetrantLock 公平鎖講解
reentrantLock 支持公平鎖和非公平鎖,默認的構造方法初始化的是非公平鎖,上文我們詳細的介紹了非公平鎖的源碼,下面來看下公平鎖怎麼實現的
2.ReentrantLock lock = new ReentrantLock(true);
//參數爲true的時候調用的是 new FairSycn();
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
來看看公平鎖的lock方法:
//ReentrantLock.FairSync.lock
final void lock() {
acquire(1);
}
//同非公平鎖的方法,區別在於tryAcquire的實現(多態)
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
//ReentrantLock.FairSync.tryAcquire
//如果等待隊列沒有等待的節點或者,等待隊列中的第一個節點爲當前線程,嘗試去獲取鎖
//區別於非公平鎖的是:非公平鎖加入等待隊列之前不去判斷有沒有別的線程在等待鎖,而直接嘗試去爭搶鎖
//公平鎖如果發現等待隊列有別的線程在等待就不去爭搶鎖,
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
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;
}
//判斷隊列中有沒有別的線程在等待當前鎖,有返回true,沒有返回false
public final boolean hasQueuedPredecessors() {
Node t = tail;
Node h = head;
Node s;
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
公平鎖每次獲取到鎖爲同步隊列中的第一個節點,保證請求資源時間上的絕對順序,而非公平鎖有可能剛釋放鎖的線程下次繼續獲取該鎖,則有可能導致其他線程永遠無法獲取到鎖,造成“飢餓”現象。
公平鎖爲了保證時間上的絕對順序,需要頻繁的上下文切換,而非公平鎖會降低一定的上下文切換,降低性能開銷。因此,ReentrantLock默認選擇的是非公平鎖,則是爲了減少一部分上下文切換,保證了系統更大的吞吐量
註解1:等待隊列的節點數據結構
static final class Node {
static final Node SHARED = new Node();
static final Node EXCLUSIVE = null;
static final int CANCELLED = 1;
static final int SIGNAL = -1;
static final int CONDITION = -2;
static final int PROPAGATE = -3;
//節點的等待狀態
volatile int waitStatus;
// 節點的前置節點指針
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;
}
Node(Thread thread, Node mode) { // Used by addWaiter
this.nextWaiter = mode;
this.thread = thread;
}
Node(Thread thread, int waitStatus) { // Used by Condition
this.waitStatus = waitStatus;
this.thread = thread;
}
}
註解2:cas操作
在加載ReentrantLock類的時候會加載如下代碼
//我們現在只需要知道該類提供了cas操作
private static final Unsafe unsafe = Unsafe.getUnsafe();
//這些偏移量是相對於aqs對象的內存位置的偏移量
//線程的狀態
private static final long stateOffset;
//等待隊列首節點的內存偏移量
private static final long headOffset;
//等待隊列尾節點的內存偏移量
private static final long tailOffset;
private static final long waitStatusOffset;
private static final long nextOffset;
// 這塊代碼的作用是,aqs類加載的時候把相關字段的內存偏移量賦值給上邊變量
//方便cas的一些操作,cas都是通過這些內存偏移量來設置上邊的變量
static {
try {
stateOffset = unsafe.objectFieldOffset
(AbstractQueuedSynchronizer.class.getDeclaredField("state"));
headOffset = unsafe.objectFieldOffset
(AbstractQueuedSynchronizer.class.getDeclaredField("head"));
tailOffset = unsafe.objectFieldOffset
(AbstractQueuedSynchronizer.class.getDeclaredField("tail"));
waitStatusOffset = unsafe.objectFieldOffset
(Node.class.getDeclaredField("waitStatus"));
nextOffset = unsafe.objectFieldOffset
(Node.class.getDeclaredField("next"));
} catch (Exception ex) { throw new Error(ex); }
}
我們在看來一下註解2的代碼
//嘗試通過 cas設置鎖狀態碼 繼承自aqs
//stateOffset 是 aqs中 state 的內存地址(相對類的起始位置的偏移量)
protected final boolean compareAndSetState(int expect, int update) {
//如果stateOffset這個內存地址的值等於expect,就把該值設爲update返回true,否則失敗返回false
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
註解3:設置鎖爲當前線程獨佔
//設置爲當前線程佔有鎖 繼承自aqs,aqs繼承自aos
protected final void setExclusiveOwnerThread(Thread thread) {
exclusiveOwnerThread = thread;
}
網上找了一張流程圖