加鎖邏輯將分成三個部分來看:
- 競爭鎖
- 加入等待隊列
- 阻塞等待
1.競爭鎖
我們先從公平鎖入手
public void lock() {
// sync的實例是new FairSync()
sync.acquire(1);
}
// 加鎖的代碼就是這幾行
public final void acquire(int arg) {
if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
上述代碼可以拆分成以下幾段:
// 競爭鎖
tryAcquire(arg)
// 加入等待隊列
addWaiter(Node.EXCLUSIVE)
// 阻塞等待
acquireQueued(node, arg)
- 競爭鎖
protected final boolean tryAcquire(int acquires) {
// 獲取當前線程
final Thread current = Thread.currentThread();
// 獲取當前state狀態
int c = getState();
// 如果當前state是沒有任何線程搶占的話
if (c == 0) {
// 如果等待隊列中有任何一個等待的節點,都不會搶占鎖
if (!hasQueuedPredecessors() &&
// CAS搶占鎖成功
compareAndSetState(0, acquires)) {
// 搶占成功后,標記當前線程已經搶占到鎖了。
setExclusiveOwnerThread(current);
// 返回加鎖成功
return true;
}
}
// 如果是同一個線程重復加鎖的情況下
else if (current == getExclusiveOwnerThread()) {
// 在這種情況下,只是簡單地操作state
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
// 因為當前線程已經加鎖成功了,再次加鎖的話,直接在state上增加加鎖次數即可。
setState(nextc);
// 返回加鎖成功
return true;
}
// 如果已經有別的線程加鎖了,或者還有很多線程在排隊等待,那么返回false加鎖失敗。
return false;
}
上述代碼分幾部分:
-
如果當前state=0,也就是沒有任何線程搶占鎖的情況下
1.1: 沒有等待隊列的情況下,可以CAS搶占鎖
1.2: 有等待隊列的話,該隊列中第一個等待節點不是當前線程,不可以搶占鎖,因為這是公平鎖。
image.png
如果當前等待隊列中還有任意節點,并且當前節點中的線程不是當前線程,說明有其他線程處于等待過程中,那么當前線程就應該乖乖排隊去。
image.png
假如當前線程是被剛剛喚醒的,并且它處于等待隊列中的第一個等待的位置,那么這個時候是可以去搶占鎖的。
- 如果已經搶占了鎖的線程就是當前線程。這種情況我們叫做重入。
示例如下:
ReentrantLock lock = new ReentrantLock();
try {
// 加鎖
lock.lock();
// 執行業務邏輯
System.out.println("獲取的鎖");
try {
// 再次獲取鎖
lock.lock();
// 執行業務邏輯
System.out.println("再次獲取的鎖");
} finally {
// 解鎖
lock.unlock();
}
} finally {
// 解鎖
lock.unlock();
}
小結一下:
- 如果當前鎖未被搶占,并且沒有其他線程等待,那么直接搶占鎖
- 如果當前鎖未被搶占,有其他線程等待,不可用搶占鎖
- 如果當前鎖被當前線程搶占了,那么直接重入即可
- 不符合上述情況,直接加鎖失敗。也就是鎖被其他線程搶占了,或者目前還有其他線程處于等待中,都會導致公平鎖加鎖失敗。
// 判斷等待隊列中是否有其他線程等待
public final boolean hasQueuedPredecessors() {
Node h, s;
// 如果等待隊列頭節點不為空,說明等待隊列已經創建出來了。否則直接返回false。
if ((h = head) != null) {
// 如果頭節點后面的節點為空,或者該節點的狀態是取消狀態
if ((s = h.next) == null || s.waitStatus > 0) {
s = null; // traverse in case of concurrent cancellation
// 從后往前遍歷,直至最后一個狀態小于等于0的節點。只有小于等于0的節點才是正常的可以競爭鎖的節點。
for (Node p = tail; p != h && p != null; p = p.prev) {
// 發現小于等于0的節點,就賦值給s
if (p.waitStatus <= 0)
s = p;
}
}
// 如果最終得到的節點不為空。有可能當前沒有任何等待的節點,s=null。
// 并且這個不為空的等待線程不是當前線程。其實就是說明前面還有其他線程排隊。
if (s != null && s.thread != Thread.currentThread())
// 返回true,說明有其他線程在排隊。
return true;
}
// 1.如果等待隊列不存在,直接返回false
// 2.如果當前等待隊列中,沒有任何其他節點的waitStatus<=0
return false;
}
至此,線程競爭鎖的邏輯就完畢了。
- 加入等待隊列
private Node addWaiter(Node mode) {
// 創建一個節點,該節點默認
// waitStatus=0, thread=currentThread
Node node = new Node(mode);
// 開啟自旋
for (;;) {
// 取出尾節點
Node oldTail = tail;
// 如果尾節點不為空
if (oldTail != null) {
// 設置node的前一個節點為尾節點
node.setPrevRelaxed(oldTail);
// CAS把尾節點設置為node
if (compareAndSetTail(oldTail, node)){
// 如果CAS設置成功,那么就把oldTail的next引用設置成node
oldTail.next = node;
// 返回node節點
return node;
}
} else {
// 如果尾節點為null,說明等待隊列還不存在,這個時候就要準備初始化等待隊列。
// 初始化完畢后繼續自旋,最終把新創建的節點添加進等待隊列
initializeSyncQueue();
}
}
}
// 初始化等待隊列。其實是一個雙向鏈表,所以只要初始化head、tail節點即可。
private final void initializeSyncQueue() {
Node h;
// CAS設置head節點。如果head節點為null,就設置為new Node()。該node節點waitStatus=0,thread=null。
if (HEAD.compareAndSet(this, null, (h = new Node())))
// 頭節點設置成功后,尾節點初始化為同一個節點。
tail = h;
}
- 初始化等待隊列
// 初始化等待隊列。其實是一個雙向鏈表,所以只要初始化head、tail節點即可。
private final void initializeSyncQueue() {
Node h;
// CAS設置head節點。如果head節點為null,就設置為new Node()。該node節點waitStatus=0,thread=null。
if (HEAD.compareAndSet(this, null, (h = new Node())))
// 頭節點設置成功后,尾節點初始化為同一個節點。
tail = h;
}
image.png
- 添加新的節點
// 創建新節點
Node node = new Node(mode);
// 取出尾節點
Node oldTail = tail;
image.png
// 設置node的前一個節點為尾節點
node.setPrevRelaxed(oldTail);
// CAS把尾節點設置為node
if (compareAndSetTail(oldTail, node)){
// 如果CAS設置成功,那么就把oldTail的next引用設置成node
oldTail.next = node;
image.png
經過上面幾步,新的節點就被添加到等待隊列中了。
有一個注意點需要提的是:
為什么判斷等待隊列是否存在,使用的是if(tail!=null),而不是if(head!=null)?
這個問題其實跟初始化等待隊列有關系,初始化的時候是使用CAS設置head節點,成功后再設置tail節點。也就是說,隊列初始化完畢的標識是tail!=null。
如果使用if(head!=null)來判斷隊列已經存在,那么有可能此時tail還沒有初始化完畢。就會導致使用tail節點的時候空指針異常。
- 阻塞等待
final boolean acquireQueued(final Node node, int arg) {
// 默認線程未被打斷
boolean interrupted = false;
try {
// 開啟自旋
for (;;) {
// 獲取當前節點的前一個節點
final Node p = node.predecessor();
// 如果前一個節點是head節點,那么就嘗試競爭鎖
if (p == head && tryAcquire(arg)) {
// 競爭鎖成功,把當前節點設置為head節點
setHead(node);
// 把前一個節點和當前節點斷開
// 因為當前節點已經設置為head節點了,之前的head就可以GC了
p.next = null; // help GC
// 返回是否當前線程被打斷。
// 這個返回結果的作用會被用在lockInterruptibly()這個方法上。
// lock()方法可忽略。
return interrupted;
}
// 判斷當前節點是否應該阻塞。
if (shouldParkAfterFailedAcquire(p, node))
// 下面這個代碼可以翻譯成:
// if(parkAndCheckInterrupt()){
// interrupted = true;
// }
interrupted |= parkAndCheckInterrupt();
}
} catch (Throwable t) {
// 拋出任何異常,都直接取消當前節點正在競爭鎖的操作
// 如果在等待隊列中,就從等待隊列中移除。
// 如果當前線程已經搶占到鎖了,那么就解鎖。
cancelAcquire(node);
// 如果當前線程已經被中斷
if (interrupted)
// 重新設置中斷信號
selfInterrupt();
// 拋出當前異常
throw t;
}
}
- 獲取當前節點的上一個節點
// 獲取當前節點的前一個節點
final Node p = node.predecessor();
final Node predecessor() {
// 上一個節點
Node p = prev;
// 如果為null,直接拋異常
if (p == null)
throw new NullPointerException();
else
// 返回上一個節點
return p;
}
- 如果上一個節點為head節點
// 獲取當前節點的前一個節點
final Node p = node.predecessor();
// 如果前一個節點是head節點,那么就嘗試競爭鎖
if (p == head && tryAcquire(arg))
image.png
- 搶占成功鎖后
// 競爭鎖成功,把當前節點設置為head節點
setHead(node);
// 把前一個節點和當前節點斷開
p.next = null;
image.png
- 判斷當前節點的狀態
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
// 獲取前一個節點的狀態
int ws = pred.waitStatus;
// 如果狀態等于-1。Node.SIGNAL的值就是-1
if (ws == Node.SIGNAL)
// 直接返回true,這個時候就要準備阻塞。
return true;
// 如果狀態值大于0,說明是要取消的節點。
if (ws > 0) {
// 跳過“取消”狀態節點
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
// ws小于等于0的話,直接把前一個節點的狀態置為-1
// 因為新創建的節點初始化狀態是0,
// 那么意味著執行到這里后,還要返回去重新自旋一次才能返回true。
pred.compareAndSetWaitStatus(ws, Node.SIGNAL);
}
// 返回false
return false;
}
- 當前線程阻塞
private final boolean parkAndCheckInterrupt() {
// 阻塞當前線程。
// 1. 調用LockSupport.unpark()才能重新喚醒被阻塞的線程。
// 2.調用thread.interrupt()也可以喚醒阻塞線程。
LockSupport.park(this);
// 判斷當前線程是否被打斷。
// 如果當前線程是被打斷的,那么返回true,否則返回false。
return Thread.interrupted();
}
小結一下:
- 先獲取當前節點的前一個節點,如果是head節點,那么嘗試競爭鎖
- 競爭鎖成功后,重置head節點,返回false(代表沒有被打斷)。
- 如果前一個節點狀態小于等于0,那么置為-1。
- 重新自旋一次,從第一步開始
- 如果前一個節點狀態等于-1,返回true,準備阻塞。
- 調用LockSupport.park()阻塞當前線程,直至unpark()或者interrupt()喚醒當前線程。
- 通過unpark()喚醒,沒有被打斷,返回false
- 通過interrupt()喚醒,被打斷,返回true。
- 被喚醒的線程又開始自旋,直至獲取到鎖后返回是否被打斷的結果。
- 如果是被打斷后獲取鎖返回,那么返回true。
- 否則返回false。
public final void acquire(int arg) {
// 嘗試獲取鎖
if (!tryAcquire(arg) &&
// addWaiter(Node.EXCLUSIVE):競爭鎖失敗后,添加到等待隊列
// acquireQueued(node, arg):阻塞等待,自旋獲取鎖后,返回判斷是否被打斷
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
// 如果被打斷,需要恢復中斷信號
selfInterrupt();
}
// 其實就是重新中斷一次。
// 因為執行過Thread.interrupted()方法后,會讓中斷信號重置為false。
static void selfInterrupt() {
Thread.currentThread().interrupt();
}
以上就是我對于公平鎖-加鎖實現的淺析。