AQS框架基本特性與結構
Java併發包當中的大多數同步器實現都是圍繞着共同的基礎行爲,比如等待隊列、獨佔獲取、共享獲取等,而這個行爲的抽象就是基於AbstractQueuedSynchronize簡稱AQS。AQS是一個抽象同步框架,可以用來實現一個依賴狀態的同步器。
AQS核心
AQS核心有三點:
- 自旋:控制線程不跳出邏輯
- LockSupport類中有park(線程阻塞)、unpark(喚醒)方法,阻塞自旋的線程,出讓資源
- CAS:保證狀態修改的原子性
AQS中的數據結構——節點和同步隊列
AQS有2個重要組成部分:
1、state同步狀態,int類型
當state的值等於0的時候,表明沒有線程佔用它。當state>0時,表示有線程在佔用它,新來的線程需要被加入到同步隊列中。
state的訪問方式有三種:
getState()
setState(int newState)
compareAndSetState(int expect, int update)
private volatile int state;
//返回當前的同步狀態
protected final int getState() {
return state;
}
//設置同步狀態
protected final void setState(int newState) {
state = newState;
}
//以CAS的方式設置當前狀態
protected final boolean compareAndSetState(int expect, int update) {
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
2、一個同步隊列
AQS維持着一個同步隊列,它是一個雙向鏈表,每一個節點都是一個Node,代表着一個線程。
static final class Node {
//標記,表示節點正在共享模式下等待
static final Node SHARED = new Node();
//標記,表示節點正在獨佔模式下等待
static final Node EXCLUSIVE = null;
//waitStatus值,表明線程已取消
static final int CANCELLED = 1;
//waitStatus值,表明此節點釋放或取消,當前節點需要喚醒下一個節點。
static final int SIGNAL = -1;
//waitStatus值,表明當前節點位於條件隊列中,它不會作爲同步隊列的節點。
static final int CONDITION = -2;
//waitStatus值,表明下一個共享式同步狀態將被無條件傳播
static final int PROPAGATE = -3;
volatile int waitStatus;
//前驅節點
volatile Node prev;
//後繼節點
volatile Node next;
//此節點持有的線程
volatile Thread thread;
//等待隊列中的後繼節點,如果當前線程是共享的,那麼該字段是SHARED
Node nextWaiter;
//如果節點在共享模式下等待,則返回true
final boolean isShared() {
return nextWaiter == SHARED;
}
//返回上一個節點,如果爲空,則拋出異常
final Node predecessor() throws NullPointerException {
Node p = prev;
if (p == null)
throw new NullPointerException();
else
return p;
}
//用於建立初始頭部或共享標記
Node() {
}
//被addWaiter使用
Node(Thread thread, Node mode) {
this.nextWaiter = mode;
this.thread = thread;
}
//被Condition使用
Node(Thread thread, int waitStatus) { // Used by Condition
this.waitStatus = waitStatus;
this.thread = thread;
}
}
同步隊列由一個個節點構成,同步器擁有首節點和尾節點,假設有一個線程獲取了鎖,在這個線程沒有釋放鎖之前,其他線程都將作爲一個Node被加入到同步隊列的尾部,這時候需要通過CAS保證其原子性,線程需要傳遞當前線程認爲的尾節點和當前節點,只有設置成功後,當前節點纔會被加入到隊列的尾部。
節點加入到同步隊列:
隊列的首節點有且只有一個,因此不需要通過CAS去設置首節點,當隊頭釋放鎖後,會喚醒後面的節點,並出隊,後繼節點將會在獲取同步狀態成功後將自己設置爲首節點。
首節點的變化:
同步狀態
AQS定義了兩種同步狀態,一種是獨佔式同步狀態,一種是共享式同步狀態。
獨佔式:只有單個線程能夠成功獲得同步狀態並執行
共享式:多個線程可以成功獲得同步狀態並執行
獨佔式同步狀態獲取
獨佔式獲取同步狀態的方法是acquire(int arg)
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
//嘗試以獨佔模式獲取
protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}
//創建節點並通過enq方法加入到同步隊列尾部
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;
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
//以自旋的方式通過CAS將當前節點加入到隊尾
enq(node);
return node;
}
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);
}
}
首先該方法會通過tryAcquire方法嘗試獲取同步狀態,如果獲取失敗,就會調用addWaiter方法構造一個獨佔式(Node.EXCLUSIVE)的節點,並利用CAP方式不斷自旋,直到將該節點加入到同步隊列尾部,通過acquireQueued方法進行自旋,直到獲取同步狀態。
獨佔式同步狀態釋放
同步器通過release方法進行同步狀態的釋放,該方法會喚醒頭結點的後繼節點線程。
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
共享式同步狀態的獲取
共享式同步狀態的獲取的方法是acquireShare。
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
//參數獲取鎖
protected int tryAcquireShared(int arg) {
throw new UnsupportedOperationException();
}
通過tryAcquireShared方法嘗試獲取鎖,如果返回值大不小於0,表示能夠獲得鎖,如果返回值小於0,就需要通過doAcquireShared不斷自旋獲得鎖。
共享式同步狀態的釋放
共享式同步狀態的釋放同步狀態的方法是releaseShared。
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
瞭解LockSupport工具
作用:LockSupport可以阻塞一個線程、喚醒一個線程、它是構建同步組件的基礎工具。
它提供了一系列的靜態方法包括park開頭的方法和unpark(Thread thread)方法分別用來阻塞和喚醒一個線程。
Condition分析
AQS的一個內部類ConditionObject是Condition的一個實現類,每一個Condition包含一個等待隊列,這個等待隊列是一個單鏈表,在每一個Condition中都會有兩個指針,一個指向頭節點,一個指向尾節點。
同步隊列與等待隊列
一個鎖可以包含有多個Condition,每一個Condition上面可能都會有多個線程進入等待狀態。
節點在隊列之間的移動
當一個線程調用await方法時,節點將從同步隊列移動到等待隊列中。
當一個線程調用signal方法時,當前Condition中的同步節點的線程就會被喚醒,該線程就會嘗試去競爭鎖,如果這個鎖被其他線程所持有,那麼當前線程就會再次被加入到同步隊列的尾部。
實現一個獨佔鎖
我們創建一個自定義鎖的類MyLock,實現Lock接口,在類中定義內部類Sync繼承抽象類AbstractQueuedSynchronizer,我們需要重寫其tryAcquire方法和tryRelease方法,因爲這兩個方法中是直接拋出了異常,同時,爲了實現newCondition方法,我們直接使用抽象類中已經實現了ConditionObject類
public class MyLock implements Lock {
//實現獨佔鎖功能
private class Sync extends AbstractQueuedSynchronizer {
//獲取鎖
@Override
protected boolean tryAcquire(int arg) {
int state = getState();
if (state == 0) { //證明能夠獲得鎖
//利用CAS原理修改state
if(compareAndSetState(0, arg)) {
//設置當前線程佔有資源
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
}
return false;
}
//釋放鎖
@Override
protected boolean tryRelease(int arg) {
int state = getState() - arg;
if (state == 0) { //判斷釋放後state是否爲0
setExclusiveOwnerThread(null);
setState(state);//設置state爲0
return true;
}
//存在線程安全嗎?no,因爲釋放鎖表明你已經獨佔了這個鎖
setState(state);//設置當前state的值
return false;
}
public Condition newConditionObject() {
return new ConditionObject();
}
}
private Sync sync = new Sync();
@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.newConditionObject();
}
}
測試我們所實現的鎖
public class Example01 {
private MyLock lock = new MyLock();
public static void main(String[] args) {
final Example01 ex = new Example01();
//創建兩個線程,執行out方法
new Thread(()->{
ex.out();
}).start();
new Thread(()->{
ex.out();
}).start();
}
public void out() {
lock.lock();
System.out.println("進入");
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
lock.unlock();//需要使用finally
System.out.println("出去");
}
}
好像可以完成我們加鎖的工作,不過,我們所實現的鎖並不是可重入鎖,下面的代碼就會出現問題。
我們創建兩個方法
public void method1() {
lock.lock();
System.out.println("method1");
method2();
lock.unlock();//需要使用finally
}
public void method2() {
lock.lock();
System.out.println("method2");
lock.unlock();//需要使用finally
}
直接使用一個線程調用method1
new Thread(()->{
ex.method1();
}).start();
我們發現,線程進入了method1,但是卻進入不了method2。
這是因爲我們獲取鎖時重寫的tryAcquire方法,將state從0變爲了arg,當我們再次訪問tryAcquire方法,state不等於0,表明有線程佔有,其直接給我們返回了false,當前線程正在等待自己釋放鎖。
我們需要對tryAcquire方法進行修改,使其支持可重入
protected boolean tryAcquire(int arg) {
int state = getState();
if (state == 0) { //證明能夠獲得鎖
//利用CAS原理修改state
if(compareAndSetState(0, arg)) {
//設置當前線程佔有資源
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
} else if(getExclusiveOwnerThread() == Thread.currentThread()) {
//判斷進入線程是不是還是當前線程
setState(getState() + arg);//不存在線程安全
return true;
}
return false;
}
當state不等於0時,我們還需要判斷,當前所佔用鎖的線程是否與現在進行tryAcquire調用的線程是同一個線程,如果是的話,那麼返回爲true,同時其state也要在原來state的基礎上增加arg。
對於釋放鎖的方法,我們並不需要去修改。