前言
本文主要參考自《Java 併發編程的藝術》第五章內容,結合源碼對書中內容進行分析補充。
I. Lock接口
在 Lock
接口出現之前,Java程序是靠 synchronized
關鍵字實現鎖功能的,而 Java SE 5 之後,併發包中新增了 Lock
接口(以及相關實現類)用來實現鎖功能,它提供了與 synchronized
關鍵字類似的同步功能,只是在使用時需要顯式地獲取和釋放鎖。使用 synchronized
關鍵字是隱式地獲取鎖,但是它將鎖的獲取和釋放固化了,必須先獲取再釋放。當然,這種方式簡化了同步的管理,可是擴展性沒有顯式的鎖獲取和釋放來的好。比如我們需要先獲取A鎖,再獲取B鎖,然後釋放A鎖,再釋放B鎖,synchronized
就難以實現了。
Lock的使用
Lock的使用很簡單,下面是一個簡單的示例:
// 創建鎖
Lock lock = new ReentrantLock();
// 獲取鎖
lock.lock();
try {
// do sth.
} finally {
// 最終確保釋放鎖
lock.unlock();
}
示例中使用了 ReentrantLock
—— 重入鎖,它是 Lock
的一個實現類,之後的文章我們會研究這個實現類的源碼。通過 Lock
接口的 lock()
和 unlock()
方法能夠進行獲取鎖和釋放鎖,在 finally
塊中釋放鎖,目的是保證在獲取到鎖之後,最終能夠被釋放。不要將獲取鎖的過程寫在 try
塊中,因爲如果在獲取鎖時如果發生了異常,異常拋出的同時,由於必須執行 finally
塊,導致鎖沒有獲取成功反而無故釋放。如果沒有寫在 try
塊中,那麼獲取鎖直接發生異常代碼直接結束。
Lock接口定義
首先,我們直接查看 Lock
的定義。
/*
* Written by Doug Lea with assistance from members of JCP JSR-166
*/
package java.util.concurrent.locks;
import java.util.concurrent.TimeUnit;
public interface Lock {
/**
* 阻塞獲取鎖,調用該方法當前線程獲取鎖,當鎖獲得後,從該方法返回。
* 如果獲取不到鎖,則當前線程將被禁用以進行線程調度,在獲取鎖之前處於等待狀態(lock中的等待相當於sychronized的blocked狀態)。
* Lock的實現需要能夠檢測到鎖的錯誤使用,例如可能導致死鎖的調用、拋出未經檢查的異常等。
* 實現方法必須記錄下這些異常情況。
*/
void lock();
/**
* 可響應中斷的獲取鎖,與lock()方法不同在於可以在當前線程獲取鎖的時候被中斷,並拋出中斷異常。
* 如果能夠馬上獲取到鎖則方法立刻返回。
*/
void lockInterruptibly() throws InterruptedException;
/**
* 嘗試非阻塞的獲取鎖,調用該方法後立刻返回,如果能夠獲取到鎖則方法返回true,獲取不到返回flase。
*
* 經典的用法如下:
* Lock lock = ...;
* if (lock.tryLock()) {
* try {
* // manipulate protected state
* } finally {
* lock.unlock();
* }
* } else {
* // perform alternative actions
* }
*
* 這種寫法保證獲取到鎖肯定被釋放,沒有獲取到鎖也不用釋放。
*/
boolean tryLock();
/**
* 當前線程超時獲取鎖,以下三種情況會從方法返回:
* 1.當前線程已經獲取鎖;
* 2.當前線程在超時時間內被中斷
* 3.超時時間結束,返回false
*/
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
/**
* 釋放鎖.
*/
void unlock();
/**
* 創建一個等待通知組件對象,該組件對象和當前的鎖實例是綁定的。當前線程只有獲取到鎖,才能調用
* 該組件的await()方法,而調用後,當前線程會釋放鎖讓線程進入等待隊列,模擬Object的等待喚醒機制。
*/
Condition newCondition();
}
相比較 synchronized
關鍵字,Lock
接口還提供了非阻塞性獲取鎖、可中斷獲取鎖以及超時獲取鎖的功能。最後的 new Condition()
方法其實和等待通知模型有關,wait()
、notify()
、notifyAll()
的功能則是由這個 Condition
相關類來實現的,這個後面的源碼分析中我們也會研究。
我們一般使用 Java API 的鎖,都用 Lock
的一些實現類,這些實現類基本都是通過聚合了一個同步器 (AbstractQueuedSynchronizer) 的子類 (Sync) 實例來完成線程訪問控制的。
II. 隊列同步器AQS
隊列同步器 AbstractQueuedSynchronizer
,是用來構建鎖或者其他同步組件的基礎框架。主要原理就是利用 volatile
的內存語義實現內存可見的,它使用了一個 volatile
類型的 int
成員變量 state 表示同步狀態,通過內置的 FIFO 隊列模擬 synchronized
的同步隊列完成想要獲取資源的所有線程的排隊阻塞以及搶佔鎖。這段話是核心,閱讀下文的時候需要一直回想此中深意。
/**
* 同步狀態,也可以理解爲共享資源數目
*/
private volatile int state;
/**
* 獲取當前同步狀態(共享資源數)
* 該操作具有讀volatile變量的內存語義
*/
protected final int getState() {
return state;
}
/**
* 設置當前同步狀態(設置資源數)
* 該操作具有寫volatile變量的內存語義
*/
protected final void setState(int newState) {
state = newState;
}
/**
* CAS設置當前狀態,該方法能夠保證狀態設置的原子性,失敗表示expect不一致了
* 該操作具有讀和寫volatile變量的內存語義
*/
protected final boolean compareAndSetState(int expect, int update) {
// See below for intrinsics setup to support this
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
同步器的主要使用方式是繼承,子類通過繼承同步器並實現它的抽象方法來管理同步狀態,在抽象方法的實現過程中免不了要對同步狀態進行更改,這時就需要使用同步器提供的對 state 成員變量進行獲取/設置的三個方法了,方法在上面代碼片中。AQS子類推薦被定義爲自定義 Lock
接口的實現類中的靜態內部類,同步器自身沒有實現任何同步接口,它僅僅是定義了一些同步狀態獲取和釋放的方法來供自定義鎖使用。同步器AQS既可以支持獨佔式地獲取同步狀態,也可以支持共享式地獲取同步狀態,這樣就可以方便實現不同類型的同步組件 (同步組件也就是各種API鎖等),比如重入鎖 ReentrantLock
、重入讀寫鎖 ReentrantReadWriteLock
和 CountDownLatcher
等。
同步器AQS與鎖的關係可以描述爲:鎖是面向使用者的,它定義了使用者與鎖交互的接口,隱藏了實現細節,比如用戶想使用鎖僅需要調用第一部分 Lock
接口中定義的方法即可;而同步器面向的是 Lock
接口方法的實現者,它簡化了鎖的實現方式,屏蔽了同步狀態管理、線程的排隊、等待與喚醒等底層操作。鎖和同步器很好地隔離了使用者和實現者所需關注的領域。
AQS的模板模式
AQS的設計是基於模板設計模式的,也就是說有一些模板方法需要我們去自己完善,需要繼承同步器並重寫指定的方法,隨後將同步器組合在自定義同步組件的實現中,並調用同步器提供的模板方法,而這些模板方法將會調用使用者重寫的方法。
① 重寫AQS中的方法
重寫AQS的方法時,需要使用上文提及的同步器提供的3個方法來訪問或修改同步狀態。AQS中定義的需重寫方法包括如下五種。
/**
* 嘗試以獨佔式的方式獲取同步狀態,實現該方法需要查詢當前狀態並
* 判斷同步狀態是否符合預期,然後再進行CAS操作設置同步狀態
*
* 該方法被想要獲取同步狀態的線程調用,如果獲取失敗,線程會被加入同步隊列,
* 直到其他線程釋放信號喚醒。該方法常常被用於實現Lock接口的tryLock()方法
*/
protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}
/**
* 獨佔式的釋放同步狀態,該方法被想要釋放同步狀態的線程調用
* 在同步隊列等待的線程則將有機會獲取到同步狀態
*/
protected boolean tryRelease(int arg) {
throw new UnsupportedOperationException();
}
/**
* 嘗試共享式的獲取同步狀態
*
* 該方法被想要獲取同步狀態的線程調用,如果獲取失敗則需要在等待同步隊列中等待
* 方法返回值大於等於0則表示獲取成功,小於0表示失敗
*/
protected int tryAcquireShared(int arg) {
throw new UnsupportedOperationException();
}
/**
* 嘗試共享式的釋放同步狀態,該方法被想要釋放同步狀態的線程調用
*/
protected boolean tryReleaseShared(int arg) {
throw new UnsupportedOperationException();
}
/**
* 當前同步器是否在獨佔模式下被當前線程佔用,一般該方法表示是否被當前線程獨佔
*
* 此方法僅在ConditionObject方法內部調用,因此如果不使用Condition,則無需重寫該方法。
*/
protected boolean isHeldExclusively() {
throw new UnsupportedOperationException();
}
② AQS的模板方法
同步器提供的模板方法基本上分爲三類:獨佔式獲取與釋放同步狀態、共享式獲取與釋放同步狀態和查詢同步隊列中的所有等待線程集合。
模板方法名稱 | 模板方法作用 |
---|---|
void acquire(int arg) | 獨佔式獲取同步狀態,如果當前線程獲取同步狀態成功,則由該方法返回,否則,將會進入同步隊列等待,該方法將會調用重寫的 tryAcquire(int arg) 方法。 |
void acquireInterruptibly(int arg) | 與 acquire(int arg) 相同,但是該方法響應中斷,當前線程如果未獲取到同步狀態則進入同步隊列中;如果當前線程被中斷,則該方法會拋出 InterruptedException 並返回。 |
boolean tryAcquireNanos(int arg, long nanosTimeout) | 在 acquireInterruptibly(int arg) 基礎上增加了超時限制,如果當前線程在超時時間內沒有獲取到同步狀態,那麼就會返回 false。如果獲取到就返回 true。 |
void acquireShared(int arg) | 共享式的獲取同步狀態,如果當前線程未獲取到同步狀態,將會進入同步隊列等待,與獨佔式獲取的主要區別是在同一時刻可以有多個線程獲取到同步狀態。 |
void acquireSharedInterruptibly(int arg) | 與 acquireShared(int arg) 相同,該方法響應中斷。 |
boolean tryAcquireSharedNanos(int arg, long nanosTimeout) | 在 acquireSharedInterruptibly(int arg) 基礎上增加了超時限制。 |
boolean release(int arg) | 獨佔式的釋放同步狀態,該方法會在釋放同步狀態之後,將同步隊列中第一個節點包含的線程喚醒。 |
boolean releaseShared(int arg) | 共享式的釋放同步狀態。 |
Collection<Thread> getQueuedThreads() | 獲取所有等待在同步隊列上的線程集合。 |
關於這些方法的源碼如下:
/**
* Acquires in exclusive mode, ignoring interrupts. Implemented
* by invoking at least once {@link #tryAcquire},
* returning on success. Otherwise the thread is queued, possibly
* repeatedly blocking and unblocking, invoking {@link
* #tryAcquire} until success. This method can be used
* to implement method {@link Lock#lock}.
*
* @param arg the acquire argument. This value is conveyed to
* {@link #tryAcquire} but is otherwise uninterpreted and
* can represent anything you like.
*/
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
/**
* Acquires in exclusive mode, aborting if interrupted.
* Implemented by first checking interrupt status, then invoking
* at least once {@link #tryAcquire}, returning on
* success. Otherwise the thread is queued, possibly repeatedly
* blocking and unblocking, invoking {@link #tryAcquire}
* until success or the thread is interrupted. This method can be
* used to implement method {@link Lock#lockInterruptibly}.
*
* @param arg the acquire argument. This value is conveyed to
* {@link #tryAcquire} but is otherwise uninterpreted and
* can represent anything you like.
* @throws InterruptedException if the current thread is interrupted
*/
public final void acquireInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
if (!tryAcquire(arg))
doAcquireInterruptibly(arg);
}
/**
* Attempts to acquire in exclusive mode, aborting if interrupted,
* and failing if the given timeout elapses. Implemented by first
* checking interrupt status, then invoking at least once {@link
* #tryAcquire}, returning on success. Otherwise, the thread is
* queued, possibly repeatedly blocking and unblocking, invoking
* {@link #tryAcquire} until success or the thread is interrupted
* or the timeout elapses. This method can be used to implement
* method {@link Lock#tryLock(long, TimeUnit)}.
*
* @param arg the acquire argument. This value is conveyed to
* {@link #tryAcquire} but is otherwise uninterpreted and
* can represent anything you like.
* @param nanosTimeout the maximum number of nanoseconds to wait
* @return {@code true} if acquired; {@code false} if timed out
* @throws InterruptedException if the current thread is interrupted
*/
public final boolean tryAcquireNanos(int arg, long nanosTimeout)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
return tryAcquire(arg) ||
doAcquireNanos(arg, nanosTimeout);
}
/**
* Acquires in shared mode, ignoring interrupts. Implemented by
* first invoking at least once {@link #tryAcquireShared},
* returning on success. Otherwise the thread is queued, possibly
* repeatedly blocking and unblocking, invoking {@link
* #tryAcquireShared} until success.
*
* @param arg the acquire argument. This value is conveyed to
* {@link #tryAcquireShared} but is otherwise uninterpreted
* and can represent anything you like.
*/
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
/**
* Acquires in shared mode, aborting if interrupted. Implemented
* by first checking interrupt status, then invoking at least once
* {@link #tryAcquireShared}, returning on success. Otherwise the
* thread is queued, possibly repeatedly blocking and unblocking,
* invoking {@link #tryAcquireShared} until success or the thread
* is interrupted.
* @param arg the acquire argument.
* This value is conveyed to {@link #tryAcquireShared} but is
* otherwise uninterpreted and can represent anything
* you like.
* @throws InterruptedException if the current thread is interrupted
*/
public final void acquireSharedInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
if (tryAcquireShared(arg) < 0)
doAcquireSharedInterruptibly(arg);
}
/**
* Attempts to acquire in shared mode, aborting if interrupted, and
* failing if the given timeout elapses. Implemented by first
* checking interrupt status, then invoking at least once {@link
* #tryAcquireShared}, returning on success. Otherwise, the
* thread is queued, possibly repeatedly blocking and unblocking,
* invoking {@link #tryAcquireShared} until success or the thread
* is interrupted or the timeout elapses.
*
* @param arg the acquire argument. This value is conveyed to
* {@link #tryAcquireShared} but is otherwise uninterpreted
* and can represent anything you like.
* @param nanosTimeout the maximum number of nanoseconds to wait
* @return {@code true} if acquired; {@code false} if timed out
* @throws InterruptedException if the current thread is interrupted
*/
public final boolean tryAcquireSharedNanos(int arg, long nanosTimeout)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
return tryAcquireShared(arg) >= 0 ||
doAcquireSharedNanos(arg, nanosTimeout);
}
/**
* Releases in exclusive mode. Implemented by unblocking one or
* more threads if {@link #tryRelease} returns true.
* This method can be used to implement method {@link Lock#unlock}.
*
* @param arg the release argument. This value is conveyed to
* {@link #tryRelease} but is otherwise uninterpreted and
* can represent anything you like.
* @return the value returned from {@link #tryRelease}
*/
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
/**
* Releases in shared mode. Implemented by unblocking one or more
* threads if {@link #tryReleaseShared} returns true.
*
* @param arg the release argument. This value is conveyed to
* {@link #tryReleaseShared} but is otherwise uninterpreted
* and can represent anything you like.
* @return the value returned from {@link #tryReleaseShared}
*/
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
/**
* 返回同步隊列中所有等待的線程集合
* 通過從尾至頭方式遍歷同步隊列的節點,獲取每個節點中保存的Thread
*/
public final Collection<Thread> getQueuedThreads() {
ArrayList<Thread> list = new ArrayList<Thread>();
for (Node p = tail; p != null; p = p.prev) {
Thread t = p.thread;
if (t != null)
list.add(t);
}
return list;
}
利用AQS實現自定義鎖
自定義同步組件/自定義鎖,需要使用同步器AQS提供的模板方法來實現自己的同步語義。而不同鎖組件邏輯實現主要是靠同步狀態的操作來實現的。同步狀態換成共享資源數目可能更貼切下面的例子。
我們通過一個自定義獨佔鎖的示例來演示一下如何自定義一個同步組件。獨佔鎖就是在同一時刻只能有一個線程獲取到鎖,而其他獲取鎖的線程只能處於同步隊列中等待,只有獲取鎖的線程釋放了鎖,後繼的線程才能夠獲取鎖。那麼同步狀態的設置可以定義爲初始資源有1個,被一個線程獲取就減1,變爲0其他線程需要在同步隊列中阻塞,等到持有資源的線程釋放資源,同步狀態又加1,同步隊列中的線程們就可以重新搶佔資源了。當然,AQS的同步隊列默認實現的先進隊列的先搶佔到資源。
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.AbstractQueuedSynchronizer;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
public class Mutex implements Lock {
// AQS的子類,重寫方法
private static class Sync extends AbstractQueuedSynchronizer {
/**
* 當同步狀態爲 0 才嘗試獲取鎖,CAS設置爲 1
*/
@Override
protected boolean tryAcquire(int arg) {
if (compareAndSetState(0, 1)) { // 獲取資源是併發獲取,需要CAS獲取
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
return false;
}
/**
* 當同步狀態爲 1 才釋放鎖
*/
@Override
protected boolean tryRelease(int arg) {
if (getState() == 0) {
throw new IllegalMonitorStateException();
}
setExclusiveOwnerThread(null);
setState(0); // 釋放鎖只有一個線程,不需要CAS
return true;
}
/**
* 是否處於獨佔狀態
*/
@Override
protected boolean isHeldExclusively() {
return getState() == 1;
}
/**
* 返回一個 ConditionObject對象,每個condition實例都包含了一個等待隊列
* @return
*/
private Condition newCondition() {
return new ConditionObject();
}
}
// 僅需要將操作代理到Sync上即可
private final Sync sync = new Sync();
// 實現Lock接口,利用AQS的模板方法
@Override
public void lock() {
sync.acquire(1);
}
@Override
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
@Override
public boolean tryLock() {
return sync.tryAcquire(1);
}
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
return sync.tryAcquireNanos(1, unit.toNanos(time));
}
@Override
public void unlock() {
sync.release(1);
}
@Override
public Condition newCondition() {
return sync.newCondition();
}
}
上述示例中,獨佔鎖Mutex是一個自定義同步組件,它在同一時刻只允許一個線程佔有鎖。Mutex
中定義了一個靜態內部類,該內部類繼承了同步器並實現了獨佔式獲取和釋放同步狀態。在 tryAcquire(int acquires)
方法中,如果經過CAS設置同步狀態爲1成功,則代表獲取了同步狀態,而在 tryRelease(int releases)
方法中只是將同步狀態重置爲0。使用 Mutex
時並不會直接和內部同步器的實現打交道,因爲 Mutex
實現了 Lock
接口,我們只需要調用接口方法即可。在實現 Lock
接口的方法時,需要利用AQS的模板方法。通過模板設計模式,大大降低了自定義同步組件的設計難度。
III. AQS源碼分析
分析AQS源碼我們着重從所有的模板方法入手,主要包括了同步隊列、獨佔式同步狀態獲取與釋放、共享式同步狀態獲取與釋放以及超時獲取同步狀態等。
同步隊列
AQS依賴內部的同步隊列(一個FIFO雙向隊列)來完成同步狀態的管理,當前線程獲取同步狀態(共享資源)失敗時,同步器會將當前線程和其等待狀態等信息構造成爲一個節點(Node)加入到同步隊列中,同時會阻塞當前線程。當同步狀態釋放時,就會把同步隊列中的第二個節點中包含的線程喚醒,讓其再次嘗試獲取同步狀態。
AQS中包含兩個成員變量,一個是頭結點還有一個尾節點,分別指向同步隊列的頭和尾。
public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer implements java.io.Serializable {
/**
* Head of the wait queue, lazily initialized. Except for
* initialization, it is modified only via method setHead. Note:
* If head exists, its waitStatus is guaranteed not to be
* CANCELLED.
*/
private transient volatile Node head;
/**
* Tail of the wait queue, lazily initialized. Modified only via
* method enq to add new wait node.
*/
private transient volatile Node tail;
static final class Node {
····
}
}
同步隊列中的節點 (Node) 用來保存獲取同步狀態失敗的線程引用、等待狀態以及前驅和後繼節點。
static final class Node {
/** 表示節點是共享模式,一個單例的SHARED節點 */
static final Node SHARED = new Node();
/** 表示節點是獨佔模式 */
static final Node EXCLUSIVE = null;
// 上面兩種模式,用途主要是賦值給nextWaiter成員變量,判斷節點是獨佔模式還是共享模式就判斷nextWaiter的值是EXCLUSIVE還是SHARED
/** waitStatus值爲CANCELLED 表示該節點中的線程被取消 */
static final int CANCELLED = 1;
/** waitStatus值爲SIGNAL 表示當前節點如果是該狀態需要喚醒後繼節點,讓後繼節點重新嘗試獲取鎖 */
static final int SIGNAL = -1;
/** waitStatus值爲CONDITION 表示線程正在等待condition通知 */
static final int CONDITION = -2;
/** waitStatus值爲PROPAGATE 表示無條件的將共享狀態傳播給下一個節點 */
static final int PROPAGATE = -3;
/**
* 等待狀態,一共有如下5種情況:
* SIGNAL: 後繼節點的線程處於等待(WAITING)狀態,而當前節點的線程如果
* 釋放了同步狀態或者被取消,將會通知後繼節點(notify),使得
* 後繼節點可以喚醒起來重新獲取鎖
* CANCELLED: 由於在同步隊列中等待的線程等待超時或者被中斷,需要從同步隊列中
* 取消等待,節點進入該狀態將不會發生變化
* CONDITION: 節點現在在等待隊列中(同步隊列與等待隊列不是一個隊列,就和synchronized的兩個隊列一樣)
* 節點中的線程等待在Condition上,當其他線程對Condition調用了signal()
* 方法後,該節點會從等待隊列中轉移到同步隊列中,加入到對同步狀態的獲取中
* PROPAGATE: 下一次共享式同步狀態獲取將會無條件的被傳播下去
* 0: 初始狀態
*/
volatile int waitStatus;
/**
* 當前節點連接的先驅節點,用於檢查waitStatus狀態。噹噹前節點入隊的時候
* 賦值先驅節點,出隊的時候置爲null爲了GC。
*/
volatile Node prev;
/**
* 當前節點連接的後繼節點
* 如果一個節點狀態設置爲取消,next將會指向自己
*/
volatile Node next;
/**
* 保存線程對象,在構造函數中初始化賦值
*/
volatile Thread thread;
/**
* 等待隊列(Condition Queue)的後繼節點。如果當前節點是共享的,那麼這個字段將是一個SHARED常量
* 也就是說節點類型(獨佔和共享)和等待隊列中的後繼節點共用一個字段。
*/
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() { // 該構造函數僅用在 SHARED標記的創建 和 同步隊列初始頭節點的產生
}
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;
}
}
節點是構成同步隊列以及等待隊列的基礎,同步器AQS擁有首節點 (head) 和尾節點 (tail),沒有成功獲取同步狀態的線程將會包裝成爲節點加入該同步隊列的尾部。
之前我們已經研究過,AQS同步器中包含了兩個 Node
類型的成員變量,一個是指向頭結點的引用 head,另一個是指向尾節點的引用 tail。同步隊列的入隊與出隊基本與雙向 FIFO 隊列的出入隊操作非常相似。首節點是獲取同步狀態成功的節點,首節點的線程在釋放同步狀態時,將會喚醒後繼節點,而後繼節點將會在獲取同步狀態成功時將自己設置爲首節點。當一個線程成功地獲取了同步狀態(或者鎖),其他線程將無法獲取到同步狀態,轉而被構造成爲節點並加入到同步隊列中。
獨佔式同步狀態獲取與釋放
① 獲取資源
通常我們調用 Lock
接口的 lock()
方法獲取鎖,lock()
方法一般藉助 AQS 的 acquire(int arg)
方法獲取同步狀態,該方法不會響應中斷,也就是說由於線程獲取同步狀態失敗後會進入同步隊列中,後續對線程進行中斷操作時,線程不會從同步隊列中移出。我們查看AQS的 acquire(int arg)
方法實現。
/**
* Acquires in exclusive mode, ignoring interrupts. Implemented
* by invoking at least once {@link #tryAcquire},
* returning on success. Otherwise the thread is queued, possibly
* repeatedly blocking and unblocking, invoking {@link
* #tryAcquire} until success. This method can be used
* to implement method {@link Lock#lock}.
*
* @param arg the acquire argument. This value is conveyed to
* {@link #tryAcquire} but is otherwise uninterpreted and
* can represent anything you like.
*/
public final void acquire(int arg) {
if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
方法的英文註釋還是較爲清晰的,會阻塞的獨佔式的獲取同步狀態。if 判斷中先執行 tryAcquire(arg)
方法,如果此時嘗試一次獲取同步狀態直接成功,則方法返回 true,由於判斷條件加了邏輯非,所以 if 判斷直接不滿足,進而 acquire(int arg)
會退出。如果 tryAcquire(arg)
方法嘗試一次獲取同步狀態失敗,那麼程序邏輯下一步會執行 acquireQueued(addWaiter(Node.EXCLUSIVE), arg)
,其實就是要加入同步隊列進行阻塞等待別的線程釋放資源了。
想加入同步隊列,先得把自己(本線程)包裝成節點,然後才能入隊。所以先通過 addWaiter(Node mode)
方法構造同步節點 (獨佔式 Node.EXCLUSIVE
同一時刻只能有一個線程成功獲取同步狀態),並將返回的新節點CAS自旋操作加入到同步隊列。通過CAS自旋操作能夠保證在多線程場景下確保節點能夠被有序正確的添加,不會發生混亂。
/**
* 爲當前線程創建指定模式mode的Node節點併入隊
* @param mode Node.EXCLUSIVE for exclusive, Node.SHARED for shared
* @return the new node
*/
private Node addWaiter(Node mode) {
// 將當前線程構造指定模式的節點
Node node = new Node(Thread.currentThread(), mode);
// 快速嘗試在隊列尾部添加一個新節點,當然很有可能不成功
Node pred = tail; // 定義舊的尾節點爲新的尾節點的前驅
if (pred != null) {
// 如果原來的尾節點存在,或者說同步隊列不爲空
node.prev = pred;
// CAS設置新的尾節點引用
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
// 運行到這,說明上面快速嘗試插入尾節點失敗了,所以再利用enq(node)進行插入。一種是隊列爲空插入失敗,另一種CAS失敗
enq(node);
return node;
}
/**
* CAS設置尾部節點
*/
private final boolean compareAndSetTail(Node expect, Node update) {
return unsafe.compareAndSwapObject(this, tailOffset, expect, update);
}
/**
* 將node尾插入同步隊列中,方法返回的是舊的尾節點
* @param node the node to insert
* @return node's predecessor
*/
private Node enq(final Node node) {
// CAS自旋入隊操作,能保證整個隊列的順序正確
for (;;) {
Node t = tail; // 這裏的tail是舊的尾節點,因爲上面CAS設置尾節點肯定失敗或者未執行
if (t == null) {
// 如果隊列爲空,需要先初始化整個隊列,做法是創建一個“空”節點設置爲head和tail,“空”是指該節點沒有包含線程
if (compareAndSetHead(new Node()))
tail = head;
// 到這沒有返回,for循環再執行一次
} else {
// 運行到這說明隊列現在是不爲空的,進行當前線程的節點入隊操作
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
// 入隊成功則返回
return t;
}
}
}
}
將當前線程的節點正確加入到同步隊列後,調用 acquireQueued(final Node node, int arg)
方法,這裏傳進來的 node
是指新的尾節點。在隊列中等待資源的每個節點都會進入一個自旋過程,每個節點/每個線程都在自省地觀察,當條件滿足,獲取到了同步狀態,就可以從自旋中退出,否則會依舊停留在自旋過程中,並且會阻塞當前線程。相當於在排隊拿號(中間沒其它事幹可以休息),直到拿到號後再返回。
/**
* 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) {
// 主要標記acquire是否成功,能否從方法返回
boolean failed = true;
try {
// 標記在等待過程中是否被中斷過
boolean interrupted = false;
// 又是一個“自旋”,節點們需要反覆的判斷是不是輪到自己獲取資源了,當然也不會一直判斷,可以的話可以“睡眠”等待喚醒
for (;;) {
final Node p = node.predecessor(); // 獲取前驅節點
// 如果前驅節點就是頭結點才輪到自己進行嘗試獲取同步狀態
if (p == head && tryAcquire(arg)) {
// 獲取同步狀態成功,則更新新的頭節點。這裏不需要用CAS設置頭結點是因爲能獲取同步狀態的線程只有一個
setHead(node);
p.next = null; // 設置前驅的後繼爲空,來幫助GC
failed = false; // 設置爲成功標記
return interrupted; // 返回是否被中斷
}
// 如果沒輪到自己獲取或者獲取失敗,看看是否能嘗試進入waiting狀態,直到被unpark()喚醒
if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
// 如果等待過程中被中斷過,哪怕只有那麼一次,就將interrupted標記爲true
interrupted = true;
}
} finally {
if (failed)
// 如果最終acquire失敗了則取消當前節點並T出同步隊列
cancelAcquire(node);
}
}
/**
* 設置隊列的頭結點引用
*/
private void setHead(Node node) {
head = node;
node.thread = null;
node.prev = null;
}
/**
* 檢查是否能進入“睡眠”狀態,必須前驅節點waitStatus爲Node.SIGNAL才能進入等待狀態,不是需設置前驅節點waitStatus爲Node.SIGNAL
* 返回線程是否需要等待(等待喚醒的那個等待)
*/
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus; // 獲取前驅的狀態
if (ws == Node.SIGNAL)
// 如果前驅節點waitStatus已經被設置成Node.SIGNAL,也就是當前驅結點同步狀態釋放或者被取消,
// 將會通知自己,那自己就可以進入等待狀態了
return true;
if (ws > 0) {
// 如果前驅節點是被取消CANCELLED狀態,那就一直往前找,
// 直到找到最近一個正常等待的狀態,並排在它的後邊。
// 這相當於一個被取消線程出隊操作!
do {
node.prev = pred = pred.prev; // pred = pred.prev; node.prev = pred;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
// 此時同步狀態爲0或者PROPAGATE,需要CAS設置成SIGNAL,讓前驅OK了通知自己
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
/**
* 讓線程進入等待狀態並查看是否被中斷
*/
private final boolean parkAndCheckInterrupt() {
// 調用LockSupport的park()使線程進入waiting等待狀態
LockSupport.park(this);
// 如果被喚醒,查看自己是不是被中斷的
return Thread.interrupted();
}
/**
* 取消正在進行的acquire操作
*/
private void cancelAcquire(Node node) {
// Ignore if node doesn't exist
if (node == null)
return;
// 清空節點保存的線程
node.thread = null;
// 往前找,跳過所有CANCELLED狀態的前驅節點
Node pred = node.prev;
while (pred.waitStatus > 0)
node.prev = pred = pred.prev;
// 新的前驅節點的後繼
Node predNext = pred.next;
// 設置當前節點同步狀態爲Node.CANCELLED
node.waitStatus = Node.CANCELLED;
// 分兩種情況進行重組同步隊列,一種是當前節點是尾節點,另一種是當前節點是隊列的中間節點
// If we are the tail, remove ourselves.
if (node == tail && compareAndSetTail(node, pred)) {
compareAndSetNext(pred, predNext, null);
} else {
// If successor needs signal, try to set pred's next-link
// so it will get one. Otherwise wake it up to propagate.
int ws;
if (pred != head &&
((ws = pred.waitStatus) == Node.SIGNAL ||
(ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
pred.thread != null) {
Node next = node.next;
if (next != null && next.waitStatus <= 0)
compareAndSetNext(pred, predNext, next);
} else {
unparkSuccessor(node);
}
node.next = node; // 自己接自己來幫助GC
}
}
代碼的註釋非常詳細,基本解釋了整個過程。主要從兩種情況來理解:
- 當同步隊列不爲空的時候,新來了一個節點入隊,判斷自己是否是排在第二,是的話就去嘗試獲取鎖,不是則看看能不能進入睡眠狀態。能不能主要是依據自己的前一個節點的
waitStatus
是不是SIGNAL
。能夠睡眠的話就不用一直自旋了,等着別的線程釋放資源喚醒自己即可; - 當同步隊列爲空,也需要將當前節點入隊,但先創建了一個“空”首節點,當然此時也是尾節點。然後自己再入隊,這樣就直接就排在第二,嘗試獲取一下鎖,獲取不到就把前面的“空節點”的
waitStatus
設置爲SIGNAL
,這樣就可以安心的睡去等待喚醒。
節點加入到同步隊列中後,進行不斷的循環嘗試獲取同步狀態(執行 tryAquire(int args)
),但是隻有自己是排在隊伍的第二個位置纔會去嘗試獲取,後面的節點則處於等待狀態。爲何如此設計呢?
- 頭結點是成功獲取到同步狀態的節點,而頭節點的線程釋放了同步狀態後,將喚醒其後繼節點,後繼節點的線程被喚醒後需要檢查自己的前驅節點是否是頭結點;
- 維護同步隊列的FIFO原則。對於隊伍中當前非頭結點,如果其前驅節點如果中途出隊(被中斷等),當前節點會從等待狀態返回,隨後檢查自己是不是第二個節點決定是否嘗試獲取同步狀態。
當前線程從 acquireQueued(addWaiter(Node.EXCLUSIVE), arg)
中返回爲 true,說明被中斷,會調用 selfInterrupt()
方法來中斷自己。如果線程在等待過程中被中斷過,它是不響應的。只是獲取資源後纔再進行自我中斷 selfInterrupt()
,將中斷補上。
/**
* 中斷當前線程
*/
static void selfInterrupt() {
Thread.currentThread().interrupt();
}
② 釋放資源
鎖釋放調用 Lock
的 unlock()
接口方法,實現 unlock()
方法常用調用AQS的 release(int args)
模板方法,從而釋放同步狀態。該方法在釋放了同步狀態後,會喚醒同步隊列中頭結點之後的第一個未取消的節點,喚醒的節點將重新嘗試獲取同步狀態。
/**
* Releases in exclusive mode. Implemented by unblocking one or
* more threads if {@link #tryRelease} returns true.
* This method can be used to implement method {@link Lock#unlock}.
*
* @param arg the release argument. This value is conveyed to
* {@link #tryRelease} but is otherwise uninterpreted and
* can represent anything you like.
* @return the value returned from {@link #tryRelease}
*/
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head; // 獲取當前隊列的頭結點
// 如果隊列爲空則不需要喚醒操作
if (h != null && h.waitStatus != 0)
// 喚醒同步隊列裏頭結點的下一個節點(中的線程),參數是隊列頭節點
unparkSuccessor(h);
return true;
}
return false;
}
/**
* 調用LockSupport的unpark()喚醒同步隊列中頭結點之後最前邊的那個未取消線程
*/
private void unparkSuccessor(Node node) {
// 獲取當前節點的同步狀態
int ws = node.waitStatus;
// 重置當前線程所在的結點waitStatus,允許失敗。
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
// 這段邏輯是在尋找node後面第一個waitStatus <= 0的節點
Node s = node.next;
if (s == null || s.waitStatus > 0) {
s = null;
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
if (s != null)
// 利用LockSupport的unpark()方法喚醒線程
LockSupport.unpark(s.thread);
}
當 s 節點被喚醒後,又再次進入 acquireQueued(final Node node, int arg)
方法的自旋中。
至於 LockSupport
中的方法後面會單獨進行討論。
③ 總結
所以,整個的流程可以概括爲:
- 當共享資源被用光了,再來想要獲取資源的線程將會進入同步隊列,此時同步隊列爲空,會創建一個“空節點”,自己排在第二位置。此時判斷自己是排在老二就會嘗試去獲取鎖,成功獲取到就會自己變成首節點,這時隊列的首節點將擁有全新的定義——最後一個獲取到資源的線程節點。如果嘗試獲取失敗,則會將前驅節點的
waitStatus
設置爲SIGNAL
,自己便可以“睡眠”了; - 如果又來了一些線程嘗試獲取資源,則肯定不會成功要入隊,此時隊列不爲空,那麼加入隊列後只需要設置前驅節點的
waitStatus
設置爲SIGNAL
,便可以“睡眠”了; - 如果有獲取到資源的線程節點釋放了資源,那麼他應該喚醒頭結點之後第一個還在“睡眠”的線程節點。在還沒有發生2的情況下,隊列中只有一個空節點和“睡眠"節點,那麼此時就直接喚醒該“睡眠"節點重新獲取元素,如果獲取到,自己變成首節點…那麼發生了2的情況下,隊列裏有很多節點等待,這時只會喚醒頭結點之後第一個還在“睡眠”的線程節點。
共享式同步狀態獲取與釋放
① 獲取
共享式獲取與獨佔式獲取主要區別在於同一時刻能否有多個線程能夠同時獲取到同步狀態,或者說能多個線程獲取到鎖。比如文件讀寫線程,如果一個讀線程已經獲取鎖,那麼其他讀線程也可以繼續獲取鎖,但是寫線程將被阻塞;如果一個寫線程已經獲取鎖,那麼它必須是獨佔的,其他任何讀線程和寫線程都必須阻塞。
通過調用AQS的 acquireShared(int arg)
方法可以共享式的獲取同步狀態,該方法實現如下:
/**
* 不響應中斷的共享式獲取鎖.
* Implemented by first invoking at least once {@link #tryAcquireShared},
* returning on success. Otherwise the thread is queued, possibly
* repeatedly blocking and unblocking, invoking {@link
* #tryAcquireShared} until success.
*
* @param arg the acquire argument. This value is conveyed to
* {@link #tryAcquireShared} but is otherwise uninterpreted
* and can represent anything you like.
*/
public final void acquireShared(int arg) {
// 先嚐試一次,沒成功則調用doAcquireShared(arg).沒成功是指tryAcquireShared(arg)的返回值小於0,也就是資源數不夠
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
/**
* 不響應中斷模式下的共享式獲取資源,arg可以是該線程需要獲取的資源數目,那麼直到剩餘資源超過arg才能成功返回
* @param arg the acquire argument
*/
private void doAcquireShared(int arg) {
// 首先,創建一個共享模式的節點,並通過CAS自旋將節點加入到同步隊列尾部
final Node node = addWaiter(Node.SHARED);
// 是否獲取成功標記
boolean failed = true;
try {
// 等待過程中是否被中斷的標記
boolean interrupted = false;
// 自旋
for (;;) {
final Node p = node.predecessor(); // 獲取當前節點的前驅
if (p == head) {
int r = tryAcquireShared(arg); // 如果是排在第二則嘗試獲取鎖
if (r >= 0) { // 獲取成功則將頭結點更新成自己,並喚醒後繼節點嘗試獲取
// 設置頭結點並向後傳播,再喚醒之後的線程
setHeadAndPropagate(node, r);
p.next = null; // help GC
if (interrupted)
selfInterrupt();
failed = false;
return;
}
}
// 前驅必須是SIGNAL才能進入等待狀態
if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
可以看到整個流程和獨佔式的代碼非常相似,主要區別在於將當前線程包裝成節點的 mode 參數是 Node.SHARED
,還有一個區別是設置頭結點時,利用的 setHeadAndPropagate(node, r)
方法,而不是簡單的 setHead()
,意思是獨佔式的方式睡眠的節點只能依靠釋放資源的節點來通知喚醒自己,而共享式的除了釋放資源的節點來通知喚醒自己,剛獲取到資源的節點也會喚醒自己。
/**
* Sets head of queue, and checks if successor may be waiting
* in shared mode, if so propagating if either propagate > 0 or
* PROPAGATE status was set.
*
* @param node the node
* @param propagate tryAcquireShared的返回值
*/
private void setHeadAndPropagate(Node node, int propagate) {
Node h = head; // 存儲下舊的head節點
setHead(node); // 設置新的head節點,也因爲單線程不需要CAS設置
/*
* 以下情況會嘗試發信號通知下一個排隊節點:
* Propagation was indicated by caller,
* or was recorded (as h.waitStatus either before
* or after setHead) by a previous operation
* (note: this uses sign-check of waitStatus because
* PROPAGATE status may transition to SIGNAL.)
* and
* The next node is waiting in shared mode,
* or we don't know, because it appears null
*
* The conservatism in both of these checks may cause
* unnecessary wake-ups, but only when there are multiple
* racing acquires/releases, so most need signals now or soon
* anyway.
* propagate > 0:還有資源
*/
if (propagate > 0 || h == null || h.waitStatus < 0 || (h = head) == null || h.waitStatus < 0) {
Node s = node.next;
if (s == null || s.isShared()) // isShared()就是由構造節點時mode參數決定的
doReleaseShared(); // 喚醒後繼
}
}
此方法在 setHead()
的基礎上多了一步,就是自己甦醒的同時,如果條件符合(比如還有剩餘資源),還會去喚醒後繼結點,畢竟是共享模式。doReleaseShared()
主要是喚醒後繼節點,因爲是共享的,下一部分會詳細研究。
在 acquireShared(int arg)
方法中,同步器調用 tryAcquireShared(arg)
方法嘗試獲取同步狀態,其返回值大於等於0時表示獲取成功。因此,在共享式獲取的自旋過程中,成功獲取到同步狀態並退出自旋的條件就是 tryAcquireShared(arg)
方法返回值大於等於0,這在 doAcquireShared(int arg)
中得以體現。失敗則進入等待狀態,直到被 unpark()/interrupt()
併成功獲取到資源才返回。整個等待過程也是忽略中斷的。
② 釋放
與獨佔式一樣,共享式獲取也需要釋放同步狀態,通過調用 releaseShared()
方法釋放同步狀態。如果成功釋放且允許喚醒等待線程,它會喚醒同步隊列裏的其他線程來獲取資源。
/**
* Releases in shared mode. Implemented by unblocking one or more
* threads if {@link #tryReleaseShared} returns true.
*
* @param arg the release argument. This value is conveyed to
* {@link #tryReleaseShared} but is otherwise uninterpreted
* and can represent anything you like.
* @return the value returned from {@link #tryReleaseShared}
*/
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) { //嘗試釋放資源
doReleaseShared(); //喚醒後繼結點
return true;
}
return false;
}
/**
* Release action for shared mode -- signals successor and ensures
* propagation. (Note: For exclusive mode, release just amounts
* to calling unparkSuccessor of head if it needs signal.)
*/
private void doReleaseShared() {
/*
* Ensure that a release propagates, even if there are other
* in-progress acquires/releases. This proceeds in the usual
* way of trying to unparkSuccessor of head if it needs
* signal. But if it does not, status is set to PROPAGATE to
* ensure that upon release, propagation continues.
* Additionally, we must loop in case a new node is added
* while we are doing this. Also, unlike other uses of
* unparkSuccessor, we need to know if CAS to reset status
* fails, if so rechecking.
*/
for (;;) {
Node h = head; // 獲得頭結點
if (h != null && h != tail) { // 當隊列至少兩個節點
int ws = h.waitStatus; // 獲取頭結點的waitStatus
if (ws == Node.SIGNAL) { // 如果waitStatus是SIGNAL
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0)) // 重置waitStatus,CAS自旋重置
continue; // loop to recheck cases
unparkSuccessor(h); // 喚醒同步隊列中頭結點之後最前邊的那個未取消線程
}
else if (ws == 0 && !compareAndSetWaitStatus(h, 0, Node.PROPAGATE)) // 如果waitStatus是0,再CAS自旋設置爲PROPAGATE
continue; // loop on failed CAS
}
if (h == head) // 上面的步驟執行完檢測head是否變化,變了需要重新執行上面的步驟
break;
}
}
整個流程主要就是釋放資源,喚醒後繼。釋放資源是 tryReleaseShared(arg)
實現的,喚醒後繼是 doReleaseShared()
實現的。在共享式獲取同步狀態時,setHeadAndPropagate()
方法最後調用了 doReleaseShared()
方法來喚醒後續節點來嘗試獲取資源。和獨佔式的主要區別在於 tryReleaseShared(arg)
方法必須確保同步狀態(或者資源數)線程安全釋放,因爲釋放操作會同時來自多個線程,所以用CAS自旋來保證。
獨佔式響應中斷獲取同步狀態
在Java5之前,當一個線程獲取不到鎖而被阻塞在 synchronized 之外時,對該線程進行中斷操作時,該線程的中斷標記位會被修改,但線程依舊被阻塞在 synchronized 上,等待着獲取鎖。這個過程就是獨佔式不響應中斷獲取鎖的感覺嘛!在Java5中,同步器提供了能夠在等待獲取同步狀態的時候被中斷立刻返回的方法。
/**
* 獨佔式響應中斷獲取同步狀態,方法只有獲取成功或中斷時返回
* 方法可以用來實現 Lock接口的lockInterruptibly方法
*
* @throws InterruptedException if the current thread is interrupted
*/
public final void acquireInterruptibly(int arg) throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
if (!tryAcquire(arg))
doAcquireInterruptibly(arg);
}
/**
* Acquires in exclusive interruptible mode.
* @param arg the acquire argument
*/
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);
}
}
可以看出和獨佔式不響應中斷的區別在於,一個是設置了一箇中斷標記,等到獲取到同步狀態後再處理中斷;另一個則是一旦發生中斷,立刻 throw new InterruptedException()
導致方法返回。
獨佔式超時獲取同步狀態
超時獲取同步狀態過程可以被看成響應中斷獲取同步狀態的“增強版”,通過調用同步器的 tryAcquireNanos(int arg, long nanosTimeout)
模板方法來進行超時獲取同步狀態,該方法在支持響應中斷的基礎上,增加了超時獲取的特性。
/**
* Attempts to acquire in exclusive mode, aborting if interrupted,
* and failing if the given timeout elapses. Implemented by first
* checking interrupt status, then invoking at least once {@link
* #tryAcquire}, returning on success. Otherwise, the thread is
* queued, possibly repeatedly blocking and unblocking, invoking
* {@link #tryAcquire} until success or the thread is interrupted
* or the timeout elapses. This method can be used to implement
* method {@link Lock#tryLock(long, TimeUnit)}.
*
* @param arg the acquire argument. This value is conveyed to
* {@link #tryAcquire} but is otherwise uninterpreted and
* can represent anything you like.
* @param nanosTimeout the maximum number of nanoseconds to wait
* @return {@code true} if acquired; {@code false} if timed out
* @throws InterruptedException if the current thread is interrupted
*/
public final boolean tryAcquireNanos(int arg, long nanosTimeout) throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
return tryAcquire(arg) ||
doAcquireNanos(arg, nanosTimeout);
}
/**
* Acquires in exclusive timed mode.
*
* @param arg the acquire argument
* @param nanosTimeout max wait time
* @return {@code true} if acquired
*/
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();
// 時間到了就返回false
if (nanosTimeout <= 0L)
return false;
if (shouldParkAfterFailedAcquire(p, node) && nanosTimeout > spinForTimeoutThreshold)
LockSupport.parkNanos(this, nanosTimeout);
if (Thread.interrupted())
// 響應中斷
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}
該方法在自旋過程中,當節點的前驅節點爲頭節點時嘗試獲取同步狀態,如果獲取成功則從該方法返回,這個過程和獨佔式同步獲取的過程類似。但是在同步狀態獲取失敗的處理上有所不同,如果當前線程獲取同步狀態失敗,則判斷是否超時 (nanosTimeout小於等於0表示已經超時),如果沒有超時,重新計算超時間隔 nanosTimeout,然後使當前線程等待 nanosTimeout 納秒 (當已到設置的超時時間,該線程會從 LockSupport.parkNanos(Object blocker, long nanos)
方法返回)。
如果 nanosTimeout 小於等於 spinForTimeoutThreshold (1000納秒) 時,將不會使該線程進行超時等待,而是進入快速的自旋過程。原因在於,非常短的超時等待無法做到十分精確,如果這時再進行超時等待,相反會讓 nanosTimeout 的超時從整體上表現得反而不精確。因此,在超時非常短的場景下,同步器會進入無條件的快速自旋。
獨佔式超時獲取同步狀態 doAcquireNanos(int arg, long nanosTimeout)
和獨佔式獲取同步狀態 acquire(int args)
在流程上非常相似,其主要區別在於未獲取到同步狀態時的處理邏輯。acquire(int args)
在未獲取到同步狀態時,將會使當前線程一直處於等待狀態,而 doAcquireNanos(int arg, long nanosTimeout)
會使當前線程等待 nanosTimeout 納秒,如果當前線程在 nanosTimeout 納秒內沒有獲取到同步狀態,將會從等待邏輯中自動返回。
共享式響應中斷獲取同步狀態
有了之前獨佔式的分析,可以發現共享式的響應中斷方式實現如出一轍。
/**
* Acquires in shared mode, aborting if interrupted. Implemented
* by first checking interrupt status, then invoking at least once
* {@link #tryAcquireShared}, returning on success. Otherwise the
* thread is queued, possibly repeatedly blocking and unblocking,
* invoking {@link #tryAcquireShared} until success or the thread
* is interrupted.
* @param arg the acquire argument.
* This value is conveyed to {@link #tryAcquireShared} but is
* otherwise uninterpreted and can represent anything
* you like.
* @throws InterruptedException if the current thread is interrupted
*/
public final void acquireSharedInterruptibly(int arg) throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
if (tryAcquireShared(arg) < 0)
doAcquireSharedInterruptibly(arg);
}
/**
* Acquires in shared interruptible mode.
* @param arg the acquire argument
*/
private void doAcquireSharedInterruptibly(int arg) throws InterruptedException {
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
for (;;) {
final Node p = node.predecessor();
if (p == head) {
int r = tryAcquireShared(arg);
if (r >= 0) {
setHeadAndPropagate(node, r);
p.next = null; // help GC
failed = false;
return;
}
}
if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
// 立即拋出異常,方法返回
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}
共享式超時獲取同步狀態
相信經過之前的分析,共享式超時獲取同步狀態的實現應該沒什麼問題了。
/**
* Attempts to acquire in shared mode, aborting if interrupted, and
* failing if the given timeout elapses. Implemented by first
* checking interrupt status, then invoking at least once {@link
* #tryAcquireShared}, returning on success. Otherwise, the
* thread is queued, possibly repeatedly blocking and unblocking,
* invoking {@link #tryAcquireShared} until success or the thread
* is interrupted or the timeout elapses.
*
* @param arg the acquire argument. This value is conveyed to
* {@link #tryAcquireShared} but is otherwise uninterpreted
* and can represent anything you like.
* @param nanosTimeout the maximum number of nanoseconds to wait
* @return {@code true} if acquired; {@code false} if timed out
* @throws InterruptedException if the current thread is interrupted
*/
public final boolean tryAcquireSharedNanos(int arg, long nanosTimeout) throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
return tryAcquireShared(arg) >= 0 ||
doAcquireSharedNanos(arg, nanosTimeout);
}
/**
* Acquires in shared timed mode.
*
* @param arg the acquire argument
* @param nanosTimeout max wait time
* @return {@code true} if acquired
*/
private boolean doAcquireSharedNanos(int arg, long nanosTimeout) throws InterruptedException {
if (nanosTimeout <= 0L)
return false;
final long deadline = System.nanoTime() + nanosTimeout;
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
for (;;) {
final Node p = node.predecessor();
if (p == head) {
int r = tryAcquireShared(arg);
if (r >= 0) {
setHeadAndPropagate(node, r);
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);
if (Thread.interrupted())
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}
IV. 自定義同步組件
設計一個同步工具,該工具同一個時刻,只允許兩個線程同時訪問,超過兩個線程訪問將會被阻塞。
首先,確定訪問模式。該工具能夠同一時刻支持多個線程同時訪問,這明顯是共享式訪問。這需要同步器提供的 acquireShared(int args)
方法和 Shared 相關的方法,同時要求重寫 tryAcquireShared(int args)
方法和 tryReleaseShared(int args)
方法,這樣才能保證同步器的共享式同步狀態的獲取與釋放方法得以執行。
其次,定義資源數量。工具在同一時刻允許兩個線程同時訪問,表明同步資源數爲2,這樣可以設置初始狀態 status 爲2,當一個線程進行獲取,status 減一,status 加一,合法狀態爲0、1、2。
自定義同步組件—TwinsLock
public class TwinsLock implements Lock {
private final Sync sync = new Sync(2);
private static final class Sync extends AbstractQueuedSynchronizer {
Sync(int count) {
if (count <= 0) {
throw new IllegalArgumentException("初始化資源需要大於0");
}
// 設置初始化狀態
setState(count);
}
@Override
protected int tryAcquireShared(int arg) {
// 自旋CAS
for (;;) {
int currentState = getState();
int newState = currentState - arg;
// 如果newState < 0沒有資源,則直接短路返回負數表示獲取失敗
if (newState < 0 || compareAndSetState(currentState, newState)) {
return newState;
}
}
}
@Override
protected boolean tryReleaseShared(int arg) {
// 自旋CAS
for (;;) {
int currentState = getState();
int newState = currentState + arg;
if (compareAndSetState(currentState, newState)) {
return true;
}
}
}
}
@Override
public void lock() {
sync.acquireShared(1);
}
@Override
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
@Override
public boolean tryLock() {
return sync.tryAcquireShared(1) > 0;
}
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
return false;
}
@Override
public void unlock() {
sync.releaseShared(1);
}
@Override
public Condition newCondition() {
return null;
}
}
測試TwinsLock
public class TwinLockTest {
public static void main(String[] args) {
final Lock lock = new TwinsLock();
class Worker extends Thread {
@Override
public void run() {
for (;;) {
lock.lock();
System.out.println(Thread.currentThread().getName() + "獲取鎖");
try {
TwinLockTest.sleep();
} finally {
System.out.println(Thread.currentThread().getName() + "釋放鎖");
lock.unlock();
TwinLockTest.sleep();
}
}
}
}
for (int i = 0; i < 10; i++) {
Worker w = new Worker();
w.setDaemon(true);
w.start();
}
for (int i = 0; i < 20; i++) {
TwinLockTest.sleep();
}
}
private static void sleep() {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
測試結果
可以看到,線程總是成對的獲取與釋放鎖。
參考文章
- Java併發之AQS詳解
- AbstractQueuedSynchronizer同步隊列與Condition等待隊列協同機制
- 《Java併發編程的藝術》第五章