AbstractQueuedSynchronizer是JUC的核心框架,其設計非常精妙。 使用了Java的模板方法模式。 首先試圖還原一下其使用場景:
對於排他鎖,在同一時刻,N個線程只有1個線程能獲取到鎖;其他沒有獲取到鎖的線程被掛起放置在隊列中,待獲取鎖的線程釋放鎖後,再喚醒隊列中的線程。
線程的掛起是獲取鎖失敗時調用Unsafe.park()方法;線程的喚醒是由其他線程釋放鎖時調用Unsafe.unpark()實現。
由於獲取鎖,執行鎖內代碼邏輯,釋放鎖整個流程可能只需要耗費幾毫秒,所以很難對鎖的爭用有一個直觀的感受。下面以3個線程來簡單模擬一下排他鎖的機制。
import sun.misc.Unsafe;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.locks.LockSupport;
public class AQSDemo {
private static final Unsafe unsafe = getUnsafe();
private static final long stateOffset;
private static Unsafe getUnsafe() {
try {
Field field = Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
return (Unsafe)field.get(null);
} catch (Exception e) {
}
return null;
}
static{
try{
stateOffset = unsafe.objectFieldOffset
(AQSDemo.class.getDeclaredField("state"));
} catch (Exception ex) { throw new Error(ex); }
}
private volatile int state;
private List<Thread> threads = new ArrayList<>();
public void lock(){
if(!unsafe.compareAndSwapInt(state,stateOffset,0,1)){
// 有問題,非線程安全;只作演示使用
threads.add(Thread.currentThread());
LockSupport.park();
Thread.interrupted();
}
}
public void unlock(){
state = 0;
if(!threads.isEmpty()){
Thread first = threads.remove(0);
LockSupport.unpark(first);
}
}
static class MyThread extends Thread{
private AQSDemo lock;
public MyThread(AQSDemo lock){
this.lock = lock;
}
public void run(){
try{
lock.lock();
System.out.println("run ");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
public static void main(String[] args) {
AQSDemo lock = new AQSDemo();
MyThread a1 = new MyThread(lock);
MyThread a2 = new MyThread(lock);
MyThread a3 = new MyThread(lock);
a1.start();
a2.start();
a3.start();
}
}
上面的代碼,使用park和unpark簡單模擬了排他鎖的工作原理。使用ArrayList屏蔽了鏈表多線程環境下鏈表的構造細節, 該代碼實際上在多線程環境中使用是有問題的,發現了麼?
通過上面的代碼,能理解到多線程環境下,鏈表爲什麼能比ArrayList好使。
理解AQS, 其核心在於理解state
和head
, tail
三個變量。換句話說,理解AQS, 只需理解狀態
和鏈表實現的隊列
這兩樣東西。其使用方式就是,如果更新狀態不成功,就把線程掛起,丟到隊列中;其他線程使用完畢後,從隊列中喚醒一個線程執行。 如果排隊的線程數量過多,那麼該誰首先獲得鎖就有講究,不能暗箱操作,所以有公平和非公平兩種策略。
越來越能理解 “編程功底,細節是魔鬼”,理解了上面的使用方式,只相當於理解了需求。那麼實現上有那些細節呢? 我們通過問答的方式來闡明。
問題1: state
變量爲什麼要用volatile關鍵詞修飾?
volatile是synchronized的輕量版本,在特定的場景下具備鎖的特點變量更新的值不依賴於當前值
, 比如setState()
方法。 當volatile的場景不滿足時,使用Unsafe.compareAndSwap即可。
問題2: 鏈表是如何保證多線程環境下的鏈式結構?
首先我們看鏈表是一個雙向鏈表,我們看鏈表呈現的幾個狀態:
1. 空鏈表
(未初始化)
head -- null
tail -- null
or
(初始化後)
head -- Empty Node
tail -- Empty Node
2. 只有一個元素的鏈表
head -- Empty Node <-> Thread Node -- tail
也就是說,當鏈表的不爲空時, 鏈表中填充者一個佔位節點。
學習數據結構,把插入刪除兩個操作弄明白,基本就明白這個數據結構了。我們先看插入操作enq()
:
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) { // Must initialize
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
首先一個無限循環。 假如這個鏈表沒有初始化,那麼這個鏈表會通過循環的結構插入2個節點。 由於多線程環境下, compareAndSet會存在失敗,所以通過循環保證了失敗重試。 爲了保證同步,要麼依賴鎖,要麼通過CPU的cas。 這裏是實現同步器,只能依賴cas。 這種編程結構,看AtomicInteger,會特別熟悉。
接下來看鏈表的刪除操作。當線程釋放鎖調用release()
方法時,AQS會按線程進入隊列的順序喚醒地一個符合條件的線程,這就是FIFO的體現。代碼如下:
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
這裏unparkSuccessor()
裏面的waitStatus
我們先忽略。這樣的話,線程會從阻塞的後面繼續執行,從parkAndCheckInterrupt()
方法中出來。
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;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
由於喚醒的順序是FIFO, 所以通常p==head
條件是滿足的。如果獲取到鎖,就把當前節點作爲鏈表的head節點:setHead(node)
, 原head節點從鏈表中斷開,讓GC回收p.next=null
。 也就是說,鏈表的刪除是從頭開始刪除,以實現FIFO的目標。
到這裏,AQS的鏈表操作就弄清楚了。接下來的疑問就在節點的waitStatus
裏面。
問題: waitStatus的作用是什麼?
在AQS, 實現了一個ConditionObject, 就像Object.wait/nofity必須在synchronized中調用一樣, JUC實現了一個Object.wait/notify的替代品。這是另一個話題,這裏不細說了,後面再研究一下。
最後,總結一下,本文簡單分析了一下AQS的實現機制。主要參考ReentrantLock和論文《The java.util.concurrent Synchronizer Framework》。