文章目錄
Lock有很多具體的鎖的實現,但最直觀的實現是ReentrantLock重入鎖,也是平時我們用的最多的。重入鎖是獨佔鎖的代表。
文章中的所有源碼全部來源於jdk1.8
ReentrantLock 重入鎖
表示可重入的鎖,舉個例子:當線程t1通過調用lock()方法獲取鎖之後,再次調用lock,是不會再阻塞獲取鎖的,直接增加重試次數就行了。synchronized 和 ReentrantLock 都是可重入鎖;
來個簡單的demo來說明一下重入性:
public class ReentrantDemo{
public synchronized void demo(){
System.out.println("begin:demo");
demo2();
}
public void demo2(){
System.out.println("begin:demo1");
synchronized (this){
}
}
public static void main(String[] args) {
ReentrantDemo rd=new ReentrantDemo();
new Thread(rd::demo).start();
}
main裏面啓動了一個線程,調用synchronized修飾的demo方法,獲得了當前的對象鎖,然後demo中調用了demo2方法,demo2中又存在同一個市裏說,此時,當前線程會因爲無法持有demo2的對象鎖而阻塞,這時就會發生死鎖。重入鎖的目的是爲了避免此種死鎖問題。
重入鎖的類圖:
ReentrantLock 核心方法
void lock() // 如果鎖可用就獲得鎖,如果鎖不可用就阻塞直到鎖釋放
void lockInterruptibly() // 和lock()方法相似, 但阻塞的線程可中斷 , 拋出java.lang.InterruptedException 異常
boolean tryLock() // 非阻塞獲取鎖;嘗試獲取鎖,如果成功返回 true
boolean tryLock(long timeout, TimeUnit timeUnit) //帶有超時時間的獲取鎖方法
void unlock() // 釋放鎖
當然,這些核心方法內部還是基於AQS來實現的
ReentrantLock 源碼分析
ReentrantLock lock = new ReentrantLock();
lock.lock();
try {
System.out.println("-------todo---------");
} finally {
lock.unlock(); // 放在finally 裏面是爲了保證鎖最終一定能被釋放
}
這裏我們以lock()爲切入點,看AQS同步隊列的實現過程;
時序圖
先來一張時序圖看看整體的流程:
1. ReentrantLock.lock()
這裏是入口了,獲取鎖的入口;sync是ReentrantLock的抽象的靜態內部類,它繼承AQS來實現重入鎖的邏輯,這個系列博客的第一篇文章介紹了AQS,AQS 是一個同步隊列,它能夠實現線程的阻塞以及喚醒, 但它並不具備業務功能, 所以在不同的同步場景中,不同的鎖會繼承 AQS 來實現對應場景的功能;
public void lock() {
sync.lock();
}
Sync 有兩個具體的實現類,分別是:
NofairSync: 表示可以存在搶佔鎖的功能,也就是說不管當前隊列上是否存在其他線程等待,新線程都有機會搶佔鎖
FailSync: 表示所有線程嚴格按照 FIFO 來獲取鎖,需要判斷是否有前驅節點;
默認走的是非公平鎖的邏輯,我們以非公平鎖爲例,看一下lock()中的實現
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
- 非公平鎖和公平鎖最大的區別在於,在非公平鎖中我搶佔鎖的邏輯是,不管有沒有線程排隊,我先上來 cas 去搶佔一下
- CAS 成功,就表示成功獲得了鎖
- CAS 失敗,調用 acquire(1)走鎖競爭邏輯
在公平鎖中,是沒有這個if判斷的,直接走 acquire(1)的邏輯;
下面我們看accquire()的邏輯
2. AQS.accquire()
acquire 是 AQS 中的核心方法,如果 CAS 操作未能成功,已經有別的線程搶到了鎖,說明 state 已經不爲 0(0是無鎖時候的初始態),此時繼續 acquire(1)操作;
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
在第一篇文章AQS裏面我們也介紹過這個獨佔鎖的主要邏輯,這裏我們再詳細分析一下具體實現;
- 通過 tryAcquire 嘗試獲取獨佔鎖,如果成功返回 true,失敗返回 false
- 如果 tryAcquire 失敗,則會通過 addWaiter 方法將當前線程封裝成 Node 添加
到 AQS 隊列尾部 - acquireQueued,將 Node 作爲參數,通過自旋去嘗試獲取鎖。
下面每個方法逐一分析:
3. NonfairSync.tryAcquire(arg)
AQS 中 tryAcquire 方法的定義,並沒有實現,而是拋出異常。這裏都留給子類做具體實現了。
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
nonfairTryAcquire纔是真正實現tryAcquire邏輯的方法:
/*
* 非公平鎖實現嘗試獲取獨佔鎖
*/
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread(); // 獲取當前執行的線程
int c = getState(); // 獲取state的值
if (c == 0) { // 無鎖狀態時(初始態),這裏再cas一次試試看能不能搶到
//cas 替換 state 的值, cas 成功表示獲取鎖成功
if (compareAndSetState(0, acquires)) {
//保存當前獲得鎖的線程,下次再來的時候不需要再嘗試競爭鎖,直接走後續重入即可:state+1
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
//如果同一個線程來獲得鎖,直接增加重入次數 ,state的值遞增
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true; // 獲取鎖成功
}
return false; // 獲取鎖失敗
}
4.AQS.addWaiter()
若tryAcquire 方法獲取鎖成功則執行任務,然後再等着釋放即可,我們這裏先分析獲取鎖失敗以後線程阻塞態的邏輯。
獲取鎖失敗則會先調用 addWaiter 將當前線程封裝成Node,入參 mode 表示當前節點的狀態,傳遞的參數是 Node.EXCLUSIVE,表示獨佔狀態。意味着重入鎖用到了 AQS 的獨佔鎖功能;EXCLUSIVE在ReentrantLock好幾處都有體現。addWaiter()的邏輯大致分爲以下幾個步驟:
- 將當前線程封裝成 Node
- 當前鏈表中的 tail 節點是否爲空,如果不爲空,則通過 cas 操作把當前線程的node 添加到 AQS 隊列
- 如果爲空或者 cas 失敗,調用 enq 將節點添加到 AQS 隊列
來看一下addWaiter()和enq()的實現;
addWaiter()
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
Node pred = tail; // tail是AQS同步隊列的隊尾,默認爲null
if (pred != null) { // tail不爲空,說明隊列存在節點
node.prev = pred; // //把當前線程的 Node 的 prev 指向 tail
if (compareAndSetTail(pred, node)) { // 通過 cas把node加入到 AQS 隊列,也就是設置爲 tail
pred.next = node; // 設置成功後,把原tail節點的next指向當前node
return node;
}
}
enq(node); //tail =null ,把node添加到同步隊列
return node;
}
enq()
enq 就是通過自旋操作把當前節點加入到隊列中
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) { // Must initialize
if (compareAndSetHead(new Node())) // 通過case初始化一個新的node,
tail = head; //初始化狀態時頭尾指向同一節點
} else {
node.prev = t;
if (compareAndSetTail(t, node)) { // 初始化成功後通過cas將節點添加到隊尾
t.next = node;
return t;
}
}
}
}
圖解分析:
當三個線程同時來競爭鎖時,調用addwaiter方法結束後的邏輯:
ThreadA 獲得了鎖資源,ThreadB和ThreadC競爭鎖,然後排隊。
5. AQS.acquireQueued()
通過addwaiter()方法把線程添加到鏈表後,會接着把node做爲參數傳遞給acquireQueued()方法,去競爭鎖;結合上一個圖解,就是會把ThreadB 和ThreadC 封裝後的node節點分別傳給acquireQueued,去競爭ThreadA執行完任務後釋放的鎖;
acquireQueued的主要邏輯如下:
- 獲取當前節點的 prev 節點
- 如果 prev 節點爲 head 節點,那麼它就有資格去爭搶鎖,調用 tryAcquire 搶佔鎖
- 搶佔鎖成功以後,把獲得鎖的節點設置爲 head,並且移除原來的初始化 head節點
- 如果獲得鎖失敗,則根據 waitStatus 決定是否需要掛起線程
- 最後,通過 cancelAcquire 取消獲得鎖的操作
/**
* Acquires in exclusive uninterruptible mode for thread already in
* queue. Used by condition wait methods as well as acquire.
* @param node the node
* @param arg the acquire argument
* @return {@code true} if interrupted while waiting
*/
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor(); // 獲得當前節點的前驅節點賦給p
if (p == head && tryAcquire(arg)) { // 若p爲head節點,則當前節點有資格去競爭鎖
setHead(node); // 將當前節點設爲頭結點,也就是當前節點獲得鎖成功,成爲新的頭結點獲得執行權
p.next = null; // 把原頭結點斷開,從同步隊列中刪除
failed = false;
return interrupted;
}
// 若此時原頭結點ThreadA還沒有完全釋放鎖,則ThreadB/ThreadC就會在tryAcquire時候返回false,
// 就會走後面的shouldParkAfterFailedAcquire方法,看下面的具體分析
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
shouldParkAfterFailedAcquire
這個方法的主要作用是,通過 Node 的狀態來判斷, ThreadB 競爭鎖失敗以後是否應該被掛起;
先來回顧一下node的五個狀態:
Node 有 5 中狀態,分別是: CANCELLED(1), SIGNAL(-1)、 CONDITION(-2)、 PROPAGATE(-3)、默認狀態(0)
- CANCELLED: 在同步隊列中等待的線程等待超時或被中斷,需要從同步隊列中取消該 Node 的結點, 其結點的 waitStatus 爲 CANCELLED,即結束狀態,進入該狀態後的結點將不會再變化
- SIGNAL: 只要前置節點釋放鎖,就會通知標識爲 SIGNAL 狀態的後續節點的線程CONDITION: 和 Condition 有關係,後續會講解
- PROPAGATE: 共享模式下, PROPAGATE 狀態的線程處於可運行狀態
- CONDITION: 只有在等待隊列纔會有的狀態,同步隊列不會出現此狀態
- 0: 初始狀態
ThreadB 競爭鎖失敗以後是否應該被掛起??
- 如果 ThreadB 的 pred 節點狀態爲 SIGNAL,那就表示可以放心掛起當前線程
- 通過循環掃描鏈表把 CANCELLED 狀態的節點移除 (去掉無效的節點)
- 修改 pred 節點的狀態爲 SIGNAL,返回 false
返回 false 時,也就是不需要掛起,返回 true,則需要調用 parkAndCheckInterrupt掛起當前線程
/**
* Checks and updates status for a node that failed to acquire.
* Returns true if thread should block. This is the main signal
* control in all acquire loops. Requires that pred == node.prev.
*
* @param pred node's predecessor holding status
* @param node the node
* @return {@code true} if thread should block
*/
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus; // 獲取前驅節點的狀態
if (ws == Node.SIGNAL) // 前驅節點會singal時候,直接返回true,掛起當前節點
/*
* This node has already set status asking a release
* to signal it, so it can safely park.
*/
return true;
if (ws > 0) {// 狀態大於0表示無效節點(只有處於cancel時大於0),取消了排隊,需要移除該節點
/*
* Predecessor was cancelled. Skip over predecessors and
* indicate retry.
*/
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0); // 從鏈表中移除該cancel態的節點,用循環保證移除成功
pred.next = node;
} else {
/*
* waitStatus must be 0 or PROPAGATE. Indicate that we
* need a signal, but don't park yet. Caller will need to
* retry to make sure it cannot acquire before parking.
*/
// 利用 cas 設置 prev 節點的狀態爲 SIGNAL(-1)
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
到這一步,若ThreadA還沒有釋放鎖,ThreadB和ThreadC只能被掛起(標紅的表示掛起)
釋放鎖的過程下篇再分析吧,有點累了額~~
(。・ˇдˇ・。)
傳送門在這裏,看看釋放鎖的邏輯
【Java併發】-- ReentrantLock 可重入鎖實現原理2 - 釋放鎖