Java編程拾遺『AQS』

上篇文章講了Java中顯式鎖Lock和ReadWriteLock的概念及簡單使用示例,本篇文章重點來看一下Java中顯式鎖的底層實現。在看這部分代碼時,我任務比較好理解的方式是帶着問題去看源碼。對於顯式鎖,我們可能會有以下問題:

  • 問題1:顯式鎖底層是基於CAS實現,細節是怎樣的?
  • 問題2:顯式鎖跟synchronized類似,無法獲取鎖時,會阻塞當前線程,如何實現阻塞的?
  • 問題3:顯式鎖提供還可以提供tryLock的操作,獲取不到鎖立即返回,如何實現?
  • 問題4:顯式鎖在等待時,可以響應中斷,如何實現?
  • 問題5:顯式鎖可以提供公平鎖和非公平鎖,如何實現?
  • 問6:讀寫鎖底層又是怎麼實現的?

帶着這些問題,我們來看Java顯式鎖的實現源碼。

1. LockSupport

顯式鎖在無法獲取到鎖時,需要阻塞當前線程,synchronized中是依賴於底層操作系統函數實現的,在顯式鎖中必然也要有類似的機制,可以阻塞申請不到鎖的線程。在顯式鎖中是依賴包java.util.concurrent.locks下的LockSupport實現的,它的基本方法有:

public static void park()
public static void parkNanos(long nanos)
public static void parkUntil(long deadline)
public static void unpark(Thread thread)

park使得當前線程放棄CPU,進入等待狀態(WAITING),操作系統不再對它進行調度,什麼時候再調度呢?有其他線程對它調用了unpark,unpark需要指定一個線程,unpark會使之恢復可運行狀態。看個例子:

public static void main(String[] args) throws InterruptedException {
    Thread t = new Thread (){
        public void run(){
            LockSupport.park();
            System.out.println("exit");
        }
    };
    t.start();    
    Thread.sleep(1000);
    LockSupport.unpark(t);
}

線程t啓動後調用park,會放棄CPU,主線程睡眠1秒鐘後,調用unpark,線程t恢復運行,輸出exit。

park不同於Thread.yield(),yield只是告訴操作系統可以先讓其他線程運行,但自己依然是可運行狀態,而park會放棄調度資格,使線程進入WAITING狀態。

需要說明的是,park是響應中斷的,當有中斷髮生時,park會返回,線程的中斷狀態會被設置

park有兩個變體:

  • parkNanos:可以指定等待的最長時間,參數是相對於當前時間的納秒數。
  • parkUntil:可以指定最長等到什麼時候,參數是絕對時間,是相對於紀元時的毫秒數。 

當等待超時的時候,它們也會返回。

park方法還有一些變體,可以指定一個對象,表示是因爲該對象進行等待的,以便於調試,通常傳遞的值是this,如下:

public static void park(Object blocker)
public static void parkNanos(Object blocker, long nanos)
public static void parkUntil(Object blocker, long deadline)

LockSupport有一個方法,可以返回一個線程的blocker對象:

public static Object getBlocker(Thread t)

這些park/unpark方法是怎麼實現的呢?與CAS方法一樣,它們也調用了Unsafe類中的對應方法,Unsafe類最終調用了操作系統的API,從程序員的角度,我們可以認爲LockSupport中的這些方法就是基本操作。

2. AbstractQueuedSynchronizer(AQS)

利用CAS和LockSupport提供的基本方法,肯定就可以實現ReentrantLock了。但Java中還有很多其他併發工具,如ReentrantReadWriteLock、Semaphore、CountDownLatch,它們的實現有很多類似的地方,爲了複用代碼,Java提供了一個抽象類AbstractQueuedSynchronizer,我們簡稱爲AQS,它簡化了併發工具的實現。像ReentrantReadWriteLock、Semaphore、CountDownLatch都是基於AQS實現的。

AQS解決了實現同步器時涉及當的大量細節問題,例如獲取同步狀態、FIFO同步隊列等。基於AQS來構建同步器可以帶來很多好處,它不僅能夠極大地減少實現工作,而且也不必處理在多個位置上發生的競爭問題。 同時AQS在設計上充分考慮了可伸縮行,因此J.U.C中所有基於AQS構建的同步器均可以獲得這個優勢。

AQS的主要使用方式是繼承,子類通過繼承同步器並實現它的抽象方法來管理同步狀態

AQS使用一個int類型的成員變量state來表示同步狀態,當state>0時表示已經獲取了鎖,當state = 0時表示釋放了鎖。它提供了三個方法(getState()、setState(int newState)、compareAndSetState(int expect,int update))來對同步狀態state進行操作,當然AQS可以確保對state的操作是安全的。

AQS通過內置的FIFO同步隊列來完成線程獲取鎖的排隊工作,如果當前線程獲取同步狀態失敗(鎖)時,AQS則會將當前線程以及等待狀態等信息構造成一個節點(Node)並將其加入同步隊列,同時會阻塞當前線程,當同步狀態釋放時,則會把節點中的線程喚醒,使其再次嘗試獲取同步狀態。AQS類聲明如下:

public abstract class AbstractQueuedSynchronizer
    extends AbstractOwnableSynchronizer
    implements java.io.Serializable {

    private transient volatile Node head;

    private transient volatile Node tail;

    private volatile int state;

    //methods
}

上述head和tail,就是內部的雙向同步隊列,state用來表示同步狀態。另外,AQS繼承了AbstractOwnableSynchronizer類,AbstractOwnableSynchronizer類中有一個Thread類型的成員變量,用於存儲當前持有鎖的線程。AbstractOwnableSynchronizer類提供getExclusiveOwnerThread方法,可以獲取當前持有鎖的線程。

2.1 方法說明

AQS主要提供瞭如下一些方法:

  • protected final int getState()

返回同步狀態的當前值

  • protected final void setState(int newState)

設置當前同步狀態

  • protected final boolean compareAndSetState(int expect, int update)

CAS修改同步狀態,如果設置成功返回true,否則返回false,該方法能夠保證狀態設置的原子性

  • protected boolean tryAcquire(int arg)

獨佔式獲取同步狀態,獲取同步狀態成功後,返回true,其他線程需要等待該線程釋放同步狀態才能獲取同步狀態。獲取同步狀態失敗,立即返回false

  • public final boolean tryAcquireNanos(int arg, long nanosTimeout) throws InterruptedException

在指定超時時間內獲取獨佔鎖同步狀態,如果當前線程在nanos時間內沒有獲取到同步狀態,那麼將會返回false,已經獲取則返回true

  • protected boolean tryRelease(int arg)

獨佔式釋放同步狀態

  • protected int tryAcquireShared(int arg)

共享式獲取同步狀態,返回值大於等於0則表示獲取成功,否則獲取失敗

  • public final boolean tryAcquireSharedNanos(int arg, long nanosTimeout) throws InterruptedException

在指定超時時間內共享鎖同步狀態,如果當前線程在nanos時間內沒有獲取到同步狀態,那麼將會返回false,已經獲取則返回true

  • protected boolean tryReleaseShared(int arg)

共享式釋放同步狀態

  • protected boolean isHeldExclusively()

當前同步器是否在獨佔式模式下被線程佔用,即表示鎖是否被當前線程所獨佔

  • public final void acquire(int arg)

獨佔式獲取同步狀態,如果當前線程獲取同步狀態成功,則由該方法返回。否則,將會進入同步隊列等待,直至獲取鎖

  • public final void acquireInterruptibly(int arg) throws InterruptedException

與acquire(int arg)相同,但是該方法響應中斷,當前線程爲獲取到同步狀態而進入到同步隊列中,如果當前線程被中斷,則該方法會拋出InterruptedException異常並返回,不會繼續等待

  • public final void acquireShared(int arg)

共享式獲取同步狀態,如果當前線程未獲取到同步狀態,將會進入同步隊列等待,與獨佔式的主要區別是在同一時刻可以有多個線程獲取到同步狀態

  • public final void acquireSharedInterruptibly(int arg) throws InterruptedException

共享式獲取同步狀態,響應中斷

  • public final boolean release(int arg)

釋放獨佔式同步狀態,該方法會在釋放同步狀態之後,將同步隊列中第一個節點包含的線程喚醒

  • public final boolean releaseShared(int arg)

共享式釋放同步狀態

2.2 同步隊列

AQS內部維護了一個同步隊列,該隊列是一個FIFO雙向隊列,AQS依賴它來完成同步狀態的管理。當前線程獲取同步狀態失敗時,AQS則會將當前線程和等待狀態等信息構造成一個節點(Node)並將其加入到同步隊列,同時會阻塞當前線程,當同步狀態釋放時,會把首節點喚醒(公平鎖),使其再次嘗試獲取同步狀態(如果是非公平鎖,當鎖被釋放時,不一定被同步隊列首節點獲取到鎖,也有可能被正在申請鎖的線程獲取到,這就是非公平性的體現)。

同步隊列中,一個節點表示一個正在等待的線程,它保存着線程的引用(thread)、狀態(waitStatus)、前驅節點(prev)、後繼節點(next),其定義如下:

static final class Node {
	volatile int waitStatus;

	volatile Node prev;

	volatile Node next;

	volatile Thread thread;

	Node nextWaiter;

	//methods
}

其中nextWaiter是顯示條件(Condition)用來組織所有等待(await)的線程節點的,這點在下面講顯示條件實現時在介紹。先來看一下同步隊列的組織形式:

head指向的節點比較特殊,假如當前只有一個線程等待鎖,那麼同步隊列也會有兩個節點。可以理解爲head節點是當前正在使用鎖的節點,該節點初始化時,prev域爲null,thread域爲null,waitStatus爲0。head節點的後繼節點纔是等待鎖的第一個線程對應的節點

3. 從AQS解釋ReentrantLock實現

3.1 ReentrantLock定義

public class ReentrantLock implements Lock, java.io.Serializable {
	
	//用於實現顯式鎖的同步對象(sync其實就是隊列同步器AQS)
	private final Sync sync;

	//AQS實現類
	abstract static class Sync extends AbstractQueuedSynchronizer {}

	//非公平鎖
	static final class NonfairSync extends Sync {}

	//公平鎖
	static final class FairSync extends Sync{}

	//默認構造函數
	public ReentrantLock() {
        sync = new NonfairSync();
    }

    //構造函數
    public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }

    //methods
}

可以看到,ReentrantLock爲了複用AQS的代碼,在內部通過繼承於AQS的同步類Sync來使用AQS提供的同步隊列等特性,這也是之前講的AQS的主要使用方式是繼承,子類通過繼承同步器並實現它的抽象方法來管理同步狀態的體現。

Sync類是個抽象類,並沒有完全實現AQS的抽象方法,這是因爲ReentrantLock提供公平鎖和非公平鎖,這兩種鎖獲取鎖的策略是不同的,不同的邏輯分別在NonfairSync和FairSync中實現。 另外,通過構造函數可以看出,ReentrantLock的默認實現是非公平鎖

先來看一下Sync、NoFairSync和FairSync的實現(這裏先省略跟獲取鎖/解鎖無關的邏輯):

abstract static class Sync extends AbstractQueuedSynchronizer {

    /**
     * 抽象方法,用於公平鎖/非公平鎖子類實現不同的獲取鎖的策略
     */
    abstract void lock();

    /**
     * 非公平嘗試獲取獨佔鎖,獲取不到立即返回false
     */
    final boolean nonfairTryAcquire(int acquires) {
        final Thread current = Thread.currentThread();
        int c = getState();
        //如果鎖沒有被佔有,立即CAS嘗試佔有鎖,這就是非公平的體現
        //因爲同步隊列中有可能有之前申請鎖阻塞的線程卻沒有優先獲取鎖
        if (c == 0) {
            if (compareAndSetState(0, acquires)) {
                setExclusiveOwnerThread(current);
                return true;
            }
        }
        //如果鎖已經被佔有,並且佔有鎖的線程是當前線程,直接修改鎖的重入次數state
        else if (current == getExclusiveOwnerThread()) {
            int nextc = c + acquires;
            if (nextc < 0) // overflow
                throw new Error("Maximum lock count exceeded");
            //此時線程是持有鎖的,直接調用set方法設置同步狀態即可
            setState(nextc);
            return true;
        }
        //除以上兩種情況,都認爲獲取鎖失敗
        return false;
    }

    /**
     * 釋放獨佔鎖同步狀態
     */
    protected final boolean tryRelease(int releases) {
        int c = getState() - releases;
        //如果當前線程非持有鎖的線程,拋異常
        if (Thread.currentThread() != getExclusiveOwnerThread())
            throw new IllegalMonitorStateException();
        boolean free = false;
        if (c == 0) {
            free = true;
            setExclusiveOwnerThread(null);
        }
        //此時線程是持有鎖的,直接調用set方法設置同步狀態即可
        setState(c);
        return free;
    }
}
static final class NonfairSync extends Sync {

    /**
     * 非公平獲取獨佔鎖,獲取不到會阻塞
     */
    final void lock() {
    	//直接CAS修改同步狀態,非公平就體現在這,新申請鎖的線程有可能優先於同步隊列的線程獲取到鎖
        if (compareAndSetState(0, 1))
            setExclusiveOwnerThread(Thread.currentThread());
        else
        	//獲取獨佔鎖,如果獲取不到會阻塞線程,直至獲取到鎖,acquire方法是AQS中定義的方法
            acquire(1);
    }

    /**
     * 非公平嘗試獲取獨佔鎖,該方法是AQS的抽象方法,方法實現直接調用Sync的nonfairTryAcquire方法
     */
    protected final boolean tryAcquire(int acquires) {
        return nonfairTryAcquire(acquires);
    }
}
static final class FairSync extends Sync {

	/**
	* 公平獲取獨佔鎖
	*/
    final void lock() {
    	//獲取獨佔鎖,跟非公平鎖相比,少了CAS嘗試,acquire方法是AQS中定義的方法
        acquire(1);
    }

    /**
     * 公平嘗試獲取獨佔鎖
     */
    protected final boolean tryAcquire(int acquires) {
        final Thread current = Thread.currentThread();
        int c = getState();
        if (c == 0) {
        	//這裏跟給公平鎖相比多了個條件,同步隊列中沒有等待鎖的線程,纔會通過CAS修改同步狀態
            if (!hasQueuedPredecessors() &&
                compareAndSetState(0, acquires)) {
                setExclusiveOwnerThread(current);
                return true;
            }
        }
        else if (current == getExclusiveOwnerThread()) {
            int nextc = c + acquires;
            if (nextc < 0)
                throw new Error("Maximum lock count exceeded");
            setState(nextc);
            return true;
        }
        return false;
    }
}

這裏可以看出,Sync類中定義了抽象方法lock(),在子類NoFairSync和FairSync中都給出了實現。在子類NoFairSync和FairSync中,除了實現了Sync類的lock方法,還覆蓋了父類AQS中的tryAcquire方法,這一點也好理解,tryAcquire方法用於嘗試獲取獨佔鎖,在公平鎖和非公平鎖模式下肯定是不同的,需要各自實現。

3.2 lock()

這裏以非公平鎖爲例,來分析一下非公平鎖加鎖方法lock的實現細節。ReentrantLock中lock方法實現如下:

public void lock() {
    sync.lock();
}

上面我們知道,通過ReentrantLock的構造函數,會對成員變量sync賦值,並且不同的構造函數,得到的sync對象是不同的(NoFairSync/FairSync)。所以當sync是非公平鎖對象時,這裏調用的肯定是NoFairSync類的lock方法:

/**
 * 非公平獲取獨佔鎖,獲取不到會阻塞
 */
final void lock() {
	//直接CAS修改同步狀態,修改成功,就成功獲取了鎖
    //非公平就體現在這,新申請鎖的線程有可能優先於同步隊列的線程獲取到鎖
    if (compareAndSetState(0, 1))
        setExclusiveOwnerThread(Thread.currentThread());
    else
    	//獲取獨佔鎖,如果獲取不到會阻塞線程,直至獲取到鎖,acquire方法是AQS中定義的方法
        acquire(1);
}

上述代碼的邏輯很簡單,當線程申請鎖時,默認會讓當前線程CAS嘗試修改同步狀態,如果修改成功,當前線程就獲取了鎖,並調用AbstractOwnableSynchronizer類的setExclusiveOwnerThread方法將持有鎖的線程設置爲當前線程。因爲上述這個直接CAS的過程,即使同步隊列已經有線程在等待鎖,鎖也有可能被後來申請鎖的線程拿到,這就是非公平的體現。當CAS修改同步狀態失敗時,就調用AQS的acquire方法獲取鎖:

public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

acquire方法實現中,首先會調用tryAcquire方法嘗試獲取鎖,關於tryAcquire方法,上面講過NoFairSync和FairSync都實現了該方法,對於非公平鎖肯定調用的是NoFairSync類的tryAcquire方法:

protected final boolean tryAcquire(int acquires) {
    return nonfairTryAcquire(acquires);
}    

/**
 * 非公平嘗試獲取獨佔鎖,獲取不到立即返回false
 */
final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    //如果鎖沒有被佔有,立即CAS嘗試佔有鎖,這也是非公平的體現
    //因爲同步隊列中有可能有之前申請鎖阻塞的線程卻沒有優先獲取鎖
    if (c == 0) {
        if (compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    //如果鎖已經被佔有,並且佔有鎖的線程是當前線程,直接修改鎖的重入次數state
    else if (current == getExclusiveOwnerThread()) {
        int nextc = c + acquires;
        if (nextc < 0) // overflow
            throw new Error("Maximum lock count exceeded");
        //此時線程是持有鎖的,直接調用set方法設置同步狀態即可
        setState(nextc);
        return true;
    }
    //除以上兩種情況,都認爲獲取鎖失敗
    return false;
}

tryAcquire方法實現比較簡單,這裏不多講了,關鍵點都在上面方法的註釋中。回到AQS的acquire方法,如果tryAcquire方法嘗試獲取鎖失敗,則會調用acquireQueued方法獲取鎖,acquireQueued方法的第一個參數是通過addWaiter方法獲取的:

/**
* 向同步隊列中插入一個節點
*/
private Node addWaiter(Node mode) {
	//新生成一個節點node,表示當前等待鎖的線程
    Node node = new Node(Thread.currentThread(), mode);

    //獲取前驅節點tail(node要插入到tail後面)
    Node pred = tail;
    //tail非null,說明同步隊列中已經有節點,只要把node插入到當前tail節點後,並將tail設置爲node即可
    if (pred != null) {
        node.prev = pred;
        //CAS原子方式設置tail指向
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    //tail爲null(同步隊列爲空)或上述CAS設置tail指向失敗,調用enq方法入隊
    enq(node);
    return node;
}
/**
* 同步隊列爲空或上述CAS設置tail指向失敗時,node入隊
*/
private Node enq(final Node node) {
    for (;;) {
        Node t = tail;
        if (t == null) {
        //tail爲null,表示同步隊列中無節點,這時候需要初始化
            if (compareAndSetHead(new Node()))
            	//head指向一個空節點,該節點Thread域爲null,waitStatus爲0
            	//理解這點很重要,第一個等待鎖的線程節點並不是同步隊列的head節點,head節點是一個空節點
            	//head節點的後繼節點是第一個等待鎖的節點
                tail = head;
        } else {
        	//node入隊
            node.prev = t;
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}

也就是講,通過addWaiter方法,返回了新入隊的等待鎖的節點引用。這裏要重點強調一下,同步隊列的head節點其實指向的是個空節點,第一個等待鎖的線程節點是head的後繼節點。下面來看acquireQueued方法實現:

/**
* 排隊獲取鎖對象
* 走到這,說明線程嘗試獲取鎖失敗,只能排隊獲取鎖
*/
final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;
    try {
        boolean interrupted = false;
        //這裏是個死循環,調出循環的條件是當前節點是head的後繼節點,並且嘗試獲取鎖成功
        for (;;) {
        	//獲取node的前驅節點p
            final Node p = node.predecessor();
            //如果node是p的後繼節點,並嘗試獲取鎖成功
            if (p == head && tryAcquire(arg)) {
            	//node已經獲取到鎖,則node節點修改爲同步隊列的head,並斷開node的prev連接
                setHead(node);
                //setHead方法已經斷開了node的prev連接,這裏在斷開之前head的next連接,之前的head節點就可以被GC回收了
                p.next = null; // help GC

                failed = false;
                //返回interrupted標誌,interrupted表示線程在等待鎖期間是否被中斷
                return interrupted;
            }
            //如果節點嘗試獲取鎖失敗,就要阻塞當前線程
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

acquireQueued方法簡單的講就是讓同步隊列中的node節點獲取鎖,獲取鎖的過程是個死循環,只有等node節點獲取到鎖,才能從循環中跳出。所以node獲取鎖的過程中可能會進行多次嘗試,每次嘗試失敗時,都要阻塞自己的線程

acquireQueued方法核心就是如下三點:

  1. 循環獲取鎖,可以獲取鎖的前提是node節點是head的後繼節點,並且嘗試獲取鎖成功
  2. 如果嘗試獲取鎖失敗就要檢查是否需要阻塞當前線程,如果node的前驅節點waitStatus是SIGNAL,則需要阻塞
  3. 則塞線程調用LockSupport.park方法,該方法是個響應中斷的方法,線程可以從阻塞中返回的前提是別的線程調用了unpark方法喚醒了當前線程

檢查等待鎖的線程是否需要阻塞的方法shouldParkAfterFailedAcquire實現:

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    int ws = pred.waitStatus;
    if (ws == Node.SIGNAL)
        //前驅節點爲SIGNAL(-1),表示前面節點的線程釋放鎖會通過unpark喚醒node,
   		//因此當node獲取鎖失敗時,需要調用park堵塞阻塞線程,等待被前面的節點喚醒
        return true;
    if (ws > 0) {
        //前驅節點狀態爲CANCLE(1),表示節點代表的線程已經不需要獲取鎖,刪除前驅節點
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node; //更新next指針,刪除取消狀態的節點
    } else {
    	//前驅節點狀態未0(初始狀態),則CAS將前驅節點狀態修改爲SIGNAL
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    //走到這說明前驅節點waitStatus狀態不是SIGNAL,當前線程不需要park,
    //在下次檢查是該方法會返回true,當前線程會park阻塞
    return false;
}

總結起來就是,如果node的前驅節點waitStatus狀態是SIGNAL(-1),那麼node代表的節點就需要阻塞等待。然後來看一下重點邏輯,當線程獲取不到鎖時,是如何實現阻塞的,就是parkAndCheckInterrupt方法:

/**
* 阻塞線程,返回線程在阻塞期間是否被中斷
*/
private final boolean parkAndCheckInterrupt() {
    //阻塞線程,調用該方法後,線程會釋放CPU,進入阻塞狀態
    LockSupport.park(this);

    //走到這裏,說明線程已經從阻塞中返回了
    //前提是其他線程調用了unpark方法,喚醒了當前線程
    return Thread.interrupted(); //返回線程中斷狀態,並清除中斷狀態
}

如果該方法返回true,說明線程在阻塞等待鎖期間被中斷過。但是從acquireQueued方法實現可以看出,即使線程在等待期間被中斷了,也只是設置了中斷標記局部變量interrupted,並不會從等待中返回,所以acquireQueued是不響應中斷的,鎖是否相應中斷,其實區別就在parkAndCheckInterrupt方法返回true時的處理上

到這裏加鎖過程基本就講完了,回到最開始的幾個問題,我們至少可以回答前兩個了:

  • 問題1:顯式鎖底層是基於CAS實現,細節是怎樣的?

顯示鎖是基於AQS實現的,AQS維護鎖的狀態是通過內部int類型的成員變量state來表示的,當state>0時表示已經獲取了鎖,當state = 0時表示釋放了鎖。加鎖的過程就是通過CAS修改state成員變量的過程。

  • 問題2:顯式鎖跟synchronized類似,無法獲取鎖時,會阻塞當前線程,如何實現阻塞的?

顯示鎖中,當嘗試獲取鎖失敗時,會調用acquireQueued方法排隊獲取鎖,這個隊列就是AQS內部維護的雙向同步隊列,只有當前線程對應的節點是雙向隊列head節點的後繼節點並且嘗試獲取鎖成功時,才表示線程最終獲取鎖成功,否則就需要繼續阻塞等待,直至滿足上述條件。阻塞等待是通過LockSupport.park方法實現的。

3.3 tryLock()

之前的文章中講過,顯示鎖中提供tryLock方法,可以避免死鎖。在持有一個鎖,獲取另一個鎖,獲取不到的時候,可以釋放已持有的鎖,給其他線程機會獲取鎖。也就是講,在tryLock方法中,如果嘗試獲取鎖失敗時,並不會去排隊獲取鎖並阻塞線程,而是會直接返回果false(獲取鎖失敗)。這裏以非公平鎖的tryLock方法爲示例:

public boolean tryLock() {
    return sync.nonfairTryAcquire(1);
}

可以看到tryLock就是直接通過調用Sync類的nofairTryAcquire方法實現的,nofairTryAcquire方法的實現之前已經講過,其實就是非公平方式嘗試獲取鎖:

/**
 * 非公平嘗試獲取獨佔鎖,獲取不到立即返回false
 */
final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    //如果鎖沒有被佔有,立即CAS嘗試佔有鎖,這也是非公平的體現
    //因爲同步隊列中有可能有之前申請鎖阻塞的線程卻沒有優先獲取鎖
    if (c == 0) {
        if (compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    //如果鎖已經被佔有,並且佔有鎖的線程是當前線程,直接修改鎖的重入次數state
    else if (current == getExclusiveOwnerThread()) {
        int nextc = c + acquires;
        if (nextc < 0) // overflow
            throw new Error("Maximum lock count exceeded");
        //此時線程是持有鎖的,直接調用set方法設置同步狀態即可
        setState(nextc);
        return true;
    }
    //除以上兩種情況,都認爲獲取鎖失敗
    return false;
}

也就是講noFairTryAcquire的前提條件是(以下條件滿足一個即可):

  • 線程在申請鎖時,鎖沒有被其它線程持有,並且當前線程成功通過CAS修改了同步狀態
  • 有線程持有鎖,但是持有鎖的線程是當前線程,此次加鎖只是一次重入,只需要將同步狀態加1即可

除了上述兩種情況,都視爲獲取鎖失敗,直接返回false。

回到最開始的問題3:顯式鎖提供還可以提供tryLock的操作,獲取不到鎖立即返回,如何實現?

顯示鎖tryLock與lock方法相比,方法最開始都會通過通過noFairTryAcquire方法嘗試獲取鎖,但是對於嘗試獲取鎖失敗的情況,tryLock直接返回false,並不會阻塞當前線程,lock方法會調用acquireQueued方法,排隊獲取鎖,如果獲取不到鎖就阻塞線程,直到成功獲取鎖。

3.4 lockInterruptibly

上面講lock方法的時候,我們知道lock方法獲取鎖是不響應中斷的。也就是講在lock方法在獲取不到鎖阻塞等待的過程中,如果線程被中斷,線程並不會從中斷中返回,還是會繼續等待獲取鎖。但是顯示鎖提供了另一個加鎖方法lockInterruptibly,可以響應中斷,如下:

public void lockInterruptibly() throws InterruptedException {
	//直接調用AQS的acquireInterruptibly方法
	sync.acquireInterruptibly(1);
}
/**
 * 響應中斷獲取獨佔鎖
 */
public final void acquireInterruptibly(int arg) throws InterruptedException {
	//獲取中斷狀態,並清除中斷標識,如果線程被中斷,直接拋異常
    if (Thread.interrupted())
        throw new InterruptedException();
    //先嚐試獲取獨佔鎖,如果獲取鎖成功,方法直接結束
    //否則調用doAcquireInterruptibly方法,以響應中斷的方式獲取獨佔鎖
    if (!tryAcquire(arg))
        doAcquireInterruptibly(arg);
}
/**
 * 以響應中斷模式獲取獨佔鎖
 * 這裏的實現跟acquireQueued基本一致,唯一的區別在於park阻塞等待過程中,線程被中斷的處理上
 */
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);
    }
}

這裏可以看到,響應中斷獲取鎖和普通獲取鎖的方法實現上基本是一致的,唯一的區別就在park阻塞等待過程中,如果線程被中斷的處理上。lock方法調用的acquireQueued方法,在發生中斷時,只是記了中斷標記,並沒有中斷線程,還是會繼續等待鎖。但是lockInterruptibly方法調用的doAcquireInterruptibly方法,在發生中斷時,直接拋InterruptedException,放棄繼續等待鎖,所以可以響應中斷。這也是最開始的問題4的答案。

3.5 unLock

unLock方法就是解鎖,按照之前加鎖的過程,我們大概能猜出解鎖操作需要做的事情:修改state同步變量、unpark喚醒在同步隊列等待鎖的線程。下面來看一下unLock方法實現:

public void unlock() {
    //直接調用ReentrantLock類的內部類Sync類的release方法釋放鎖
    sync.release(1);
}
/**
 * 釋放線程持有的鎖
 */
public final boolean release(int arg) {
    //嘗試釋放鎖(修改同步狀態state)
    if (tryRelease(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}
/**
 * 嘗試釋放鎖
 */
protected final boolean tryRelease(int releases) {
    int c = getState() - releases;
    //如果當前線程並沒有持有鎖,拋異常
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    //鎖是否被完全釋放(因爲可以重入)
    boolean free = false;
    if (c == 0) {
    	//如果鎖完全釋放,將持有鎖的線程設置爲null
        free = true;
        setExclusiveOwnerThread(null);
    }
    //此刻線程還持有鎖,所以直接調用set方法修改state即可,不用CAS
    setState(c);
    return free;
}
/**
 * 鎖被完全釋放後,unpark喚醒等待隊列中等待的線程
 */
private void unparkSuccessor(Node node) {
	//顯式獨佔鎖解鎖時,這裏的node爲雙向同步隊列的head指向的節點
    int ws = node.waitStatus;
    //如果head節點的waitStatus狀態爲SIGNAL(-1),說明後面的節點需要被unpark喚醒競爭鎖
    if (ws < 0)
        compareAndSetWaitStatus(node, ws, 0);

    //尋找node後第一個沒有被取消的節點
    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;
    }
    //喚醒node後第一個沒有被取消的節點
    if (s != null)
        LockSupport.unpark(s.thread);
}

釋放鎖,也就是修改AQS同步對象state,並unpark喚醒同步隊列中等待鎖的線程的過程。

3.6 公平和非公平性

關於公平和非公平的實現,看似一個非常複雜的問題,但ReentrantLock實現方式是很簡單的。根本區別就在ReentrantLock類的內部類FairSync和NoFairSync方法tryAcquire實現上,對於非公平鎖,允許後請求鎖的線程直接獲取鎖,即使同步隊列中已經有線程在等待。而對於公平鎖而言,如果同步隊列中有等待鎖的線程,必須入隊等待,等隊頭的節點一一獲取鎖並釋放後,才能獲取鎖。代碼之前都解釋過:

  • 非公平鎖
/**
 * 非公平嘗試獲取獨佔鎖,該方法是AQS的抽象方法,方法實現直接調用Sync的nonfairTryAcquire方法
 */
protected final boolean tryAcquire(int acquires) {
    return nonfairTryAcquire(acquires);
}

/**
 * 非公平嘗試獲取獨佔鎖,獲取不到立即返回false
 */
final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    //如果鎖沒有被佔有,立即CAS嘗試佔有鎖,這就是非公平的體現
    //因爲同步隊列中有可能有之前申請鎖阻塞的線程卻沒有優先獲取鎖
    if (c == 0) {
        if (compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    //如果鎖已經被佔有,並且佔有鎖的線程是當前線程,直接修改鎖的重入次數state
    else if (current == getExclusiveOwnerThread()) {
        int nextc = c + acquires;
        if (nextc < 0) // overflow
            throw new Error("Maximum lock count exceeded");
        //此時線程是持有鎖的,直接調用set方法設置同步狀態即可
        setState(nextc);
        return true;
    }
    //除以上兩種情況,都認爲獲取鎖失敗
    return false;
}
  • 公平鎖
/**
 * 公平嘗試獲取獨佔鎖
 */
protected final boolean tryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
    	//這裏跟給公平鎖相比多了個條件,同步隊列中沒有等待鎖的線程,纔會通過CAS修改同步狀態
        if (!hasQueuedPredecessors() &&
            compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    else if (current == getExclusiveOwnerThread()) {
        int nextc = c + acquires;
        if (nextc < 0)
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    return false;
}

這裏問題5也能解釋了。

4. 從AQS解釋顯示條件Condition實現

上篇文章介紹過顯示條件Condition的使用,其實顯示條件的實現也是基於AQS實現的,這裏我們分析顯示條件是如何實現的。在看源碼時,我自己有一個技巧,在真正看之前,先徹底搞明白這個東西到底是幹嘛的,需要做哪些工作才能完成這些功能。然後帶着疑問去看源碼,這樣往往比較好理解。

回到顯示條件,我們肯定知道顯示條件的await方法的作用就是讓線程釋放鎖並進入WAITING狀態等待,直至其他線程調用signal喚醒重新去競爭鎖。能調用await和signal的前提是,當前線程是持有鎖的。

顯示條件的實現依賴於一個單向隊列,隊列的每個節點複用了上文同步隊列的Node節點(爲了區別於之前的同步隊列,我們之後管這個單向隊列叫Condition隊列)。Node類中有一個Node類型的成員變量nextWaiter就是Condition隊列中使用的(在同步操作中並沒有使用nextWaiter)。在JUC中,不止一個類要使用到顯式鎖,所以AQS中提供了一個內部類ConditionObject,在ConditionObject類中實現了await、signal的邏輯。ConditionObject類聲明如下:

public class ConditionObject implements Condition, java.io.Serializable {
    /** First node of condition queue. */
    private transient Node firstWaiter;

    /** Last node of condition queue. */
    private transient Node lastWaiter;

    //構造函數
    public ConditionObject() { }

    //methods,包括await、signal等方法
}  

4.1 await

public final void await() throws InterruptedException {
    // 如果等待前中斷標誌位已被設置,直接拋異常
    if (Thread.interrupted())
        throw new InterruptedException();
    // 1.爲當前線程創建節點,加入條件等待隊列
    Node node = addConditionWaiter();
    // 2.釋放持有的鎖
    int savedState = fullyRelease(node);
    int interruptMode = 0;
    // 3.放棄CPU,進行等待,直到被中斷或isOnSyncQueue變爲true
    // isOnSyncQueue爲true表示節點被其他線程從Condition隊列
    // 移到了同步隊列,等待的條件已滿足
    while (!isOnSyncQueue(node)) {
        LockSupport.park(this);
        if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
            break;
    }
    // 4.重新獲取鎖
    if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
        interruptMode = REINTERRUPT;
    //清除Condition隊列中所有CANCELLED狀態的節點
    if (node.nextWaiter != null)
        unlinkCancelledWaiters();
    // 5.處理中斷,拋出異常或設置中斷標誌位
    if (interruptMode != 0)
        reportInterruptAfterWait(interruptMode);
}

可以看到,顯示條件的實現是通過一個單向Condition隊列實現的,await方法首先會創建一個Node節點並插入到Condition隊列尾部,然後釋放對鎖的持有,park釋放CPU資源進入WAITING狀態,直至當前節點從Condition隊列轉移至同步隊列(其他線程調用了signal方法,將node添加到同步隊列,並喚醒node對應的線程)。線程會喚醒後會調用acquireQueued方法排隊獲取鎖,獲取鎖成功後才能從await方法返回

4.2 awaitNanos

public final long awaitNanos(long nanosTimeout) throws InterruptedException {
    if (Thread.interrupted())
        throw new InterruptedException();
    Node node = addConditionWaiter();
    int savedState = fullyRelease(node);
    long lastTime = System.nanoTime();
    int interruptMode = 0;
    while (!isOnSyncQueue(node)) {
        if (nanosTimeout <= 0L) {
            //等待超時,將節點從條件等待隊列移到外部的鎖等待隊列
            transferAfterCancelledWait(node);
            break;
        }
        //限定等待的最長時間
        LockSupport.parkNanos(this, nanosTimeout);
        if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
            break;

        long now = System.nanoTime();
        //計算下次等待的最長時間
        nanosTimeout -= now - lastTime;
        lastTime = now;
    }
    if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
        interruptMode = REINTERRUPT;
    if (node.nextWaiter != null)
        unlinkCancelledWaiters();
    if (interruptMode != 0)
        reportInterruptAfterWait(interruptMode);
    return nanosTimeout - (System.nanoTime() - lastTime);
}

實現跟await基本一致,不同的是,添加了一個超時時間控制。

4.3 signal

public final void signal() {
    //驗證當前線程持有鎖
    if (!isHeldExclusively())
        throw new IllegalMonitorStateException();
    //調用doSignal將Condition隊列中第一個節點添加到同步隊列,並unpark喚醒節點對應的線程
    Node first = firstWaiter;
    if (first != null)
        doSignal(first);
}

調用signal方法後,會將Condition隊列中等待的第一個節點添加到同步隊列並unpark喚醒線程,這時候await方法會從park中返回去競爭鎖。

5. 從AQS解釋ReentrantReadWriteLock實現

前面一篇文章介紹了讀寫鎖ReentrantReadWriteLock的使用細節,我們瞭解到ReentrantReadWriteLock相比於ReentrantLock是一種更加細粒度的鎖,將讀取與寫入分開處理,在讀取數據之前必須獲取用來讀取的鎖定,而寫入的時候必須獲取用來寫入的鎖定。並且允許多個線程同時獲取讀鎖,但是同時只允許一個線程獲取寫鎖。從而達到“讀讀”不互斥、“讀寫”互斥、“寫寫”互斥的效果。

ReentrantReadWriteLock在實現上也是通過AQS實現的,通過上面介紹的ReentrantLock源碼分析,我們知道AQS通過sate狀態進行鎖的獲取與釋放,同時構造了雙向FIFO同步隊列進行線程節點的等待,線程節點通過waitStatus來判斷自己需要掛起還是喚醒去獲取鎖。如果已經掌握了AQS的原理,那麼ReentrantReadWriteLock的實現也是非常好理解的。先看一下ReentrantReadWriteLock類聲明:

public class ReentrantReadWriteLock
        implements ReadWriteLock, java.io.Serializable {
    
    /** Inner class providing readlock */
    private final ReentrantReadWriteLock.ReadLock readerLock;

    /** Inner class providing writelock */
    private final ReentrantReadWriteLock.WriteLock writerLock;

    /** Performs all synchronization mechanics */
    final Sync sync;

    abstract static class Sync extends AbstractQueuedSynchronizer {}

    static final class NonfairSync extends Sync {}

    static final class FairSync extends Sync {}

    public static class ReadLock implements Lock, java.io.Serializable {}

    public static class WriteLock implements Lock, java.io.Serializable {}

    //other methods

}

簡單介紹一下ReentrantReadWriteLock內部類的作用:

  • Sync:繼承AQS,鎖功能的主要實現者
  • FairSync:繼承Sync,實現公平鎖
  • NofairSync:繼承Sync,主要實現非公平鎖
  • ReadLock:讀鎖,通過成員變量sync實現讀鎖功能
  • WriteLock:寫鎖,通過成員變量sync實現寫鎖功能
abstract static class Sync extends AbstractQueuedSynchronizer {
    private static final long serialVersionUID = 6317671515068378041L;    

    /*
     * Read vs write count extraction constants and functions.
     * Lock state is logically divided into two unsigned shorts:
     * The lower one representing the exclusive (writer) lock hold count,
     * and the upper the shared (reader) hold count.
     */    static final int SHARED_SHIFT   = 16;
    
    static final int SHARED_UNIT    = (1 << SHARED_SHIFT);
    static final int MAX_COUNT      = (1 << SHARED_SHIFT) - 1;
    static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;    

    /** Returns the number of shared holds represented in count  */
    static int sharedCount(int c)    { return c >>> SHARED_SHIFT; }

    /** Returns the number of exclusive holds represented in count  */
    static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }

    //……
}

ReentrantReadWriteLock的內部類Sync繼承了AQS,主要提供了鎖功能的實現,但是跟ReentrantLock的內部類Sync不同的是上面這幾個final類型的int型成員變量,除此之外,方法定義基本一致(因爲都繼承了AQS),這裏提前介紹一下這幾個成員變量的作用。我們知道AQS是通過int型成員變量state來維護同步狀態的,當state>0時表示已經獲取了鎖,當state = 0時表示釋放了鎖。但是在讀寫鎖中,讀鎖和寫鎖是分開的,如何使用一個變量state表示兩種鎖的同步狀態?答案是通過位運算,將int型成員變量的32爲分爲高16位和低16位,高16位表示讀鎖,低16位表示寫鎖。所以再回頭看上述Sync類的方法sharedCount,其實就是獲取c低16位的值,即讀鎖被持有的數量。exclusiveCount,其實就是獲取c高16位的值,即寫鎖被持有的數量。這裏再簡單回顧一下上篇文章講的讀鎖和寫鎖的進入條件:

  • 線程進入讀鎖的條件(滿足其中一個即可):
    • 沒有任何線程持有寫鎖
    • 有線程持有寫鎖,但是持有寫鎖的線程是當前線程
  • 線程進入寫鎖的條件(滿足其中一個即可)
    • 沒有任何線程持有讀鎖或寫鎖
    • 有線程持有寫鎖,但是持有寫鎖的線程是當前線程

5.1 寫鎖lock

/**
* 阻塞獲取寫鎖
*/
public void lock() {
    sync.acquire(1);
}

public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

看過上面講的ReentrantLock獲取鎖,寫鎖lock的實現代碼一定非常熟悉,實現跟ReentrantLock的lock方法完全一致。也是調用了Sync類的acquire方法(繼承自AQS),然後在acquire方法中通過回調子類的tryAcquire方法嘗試獲取鎖,其他的邏輯都不再單獨介紹了,這裏重點介紹ReentrantReadWriteLock內部類Sync的tryAcquire邏輯實現,tryAcquire其實就是非阻塞嘗試獲取寫鎖,如下:

protected final boolean tryAcquire(int acquires) {
    //獲取當前線程
    Thread current = Thread.currentThread();
    
    //獲取同步狀態
    int c = getState();

    //獲取寫鎖持有的數量
    int w = exclusiveCount(c);

    //同步狀態不等於0,說明已經鎖已經被獲取過了(可以是讀鎖也可以是寫鎖)
    if (c != 0) {
        //1. c!=0說明是有鎖被獲取的,那麼w==0,
        //說明寫鎖是沒有被獲取,也肯定是讀鎖被獲取了,
        //由於寫鎖和讀鎖的互斥,所以獲取獲取寫鎖失敗,返回false
        //2. w!=0,說明寫鎖被獲取了,但是current != getExclusiveOwnerThread() ,
        // 說明是被別的線程獲取了,return false;
        if (w == 0 || current != getExclusiveOwnerThread())
            return false;
        //走到這說明w!=0 && current == getExclusiveOwnerThread()
        //當前持有寫鎖的線程跟請求寫鎖的線程是同一線程,本次加鎖是寫鎖重入
        if (w + exclusiveCount(acquires) > MAX_COUNT)
            throw new Error("Maximum lock count exceeded");
        //寫鎖重入,直接調用setState方法修改同步狀態
        setState(c + acquires);
        return true;
    }
    // c == 0,說明沒有線程持有鎖,CAS嘗試獲取鎖
    //writerShouldBlock方法在FairSync和NoFairSync中各自實現,分別保證公平和非公平性
    if (writerShouldBlock() ||
        !compareAndSetState(c, c + acquires))
        return false;
    setExclusiveOwnerThread(current);
    return true;
}

關鍵點都在註釋中說明了,比較好理解。我們回顧一下ReentrantLock中tryAcquire的實現,ReentrantLock中內部類FairSync和NoFairSync都重寫了AQS的tryAcquire方法,但是實現邏輯都是首先處理c==0(鎖沒有被獲取)直接CAS(公平鎖需要另外判斷同步隊列是否有排隊的線程),然後處理c!=0,判斷是否爲鎖重入。ReentrantReadWriteLock中,只在Sync類中重寫了tryAcquire邏輯,FairSync和NoFairSync中並沒有重寫該方法,而是提供了writerShouldBlock和readerShouldBlock方法供Sync類的tryAcquire方法調用,判斷是否可以直接CAS競爭鎖,從而體現公平性和非公平性。在實現邏輯上,跟ReentrantLock中是一致的,只不過在ReentrantReadWriteLock中先判斷的c!=0的情況,然後判斷c==0的情況,跟ReentrantLock中處理順序有點差別。

static final class NonfairSync extends Sync {

    /**
    * 非公平鎖模式下請求寫鎖的線程是否需要排隊獲取鎖
    */
    final boolean writerShouldBlock() {
        //直接競爭,不保證公平性
        return false;
    }

    /**
    * 非公平鎖模式下請求讀鎖的線程是否需要排隊獲取鎖
    */
    final boolean readerShouldBlock() {
        //理論上也可以直接返回false,但是爲了避免同步隊列中等待的寫鎖飢餓
        //所以做了額外的判斷,這裏犧牲了一定的不公平性
        return apparentlyFirstQueuedIsExclusive();
    }
}

static final class FairSync extends Sync {
    
    /**
    * 公平鎖模式下請求寫鎖的線程是否需要排隊獲取鎖
    */
    final boolean writerShouldBlock() {
        //如果同步隊列中有排隊的線程,當前線程請求鎖就需要排隊
        return hasQueuedPredecessors();
    }

    /**
    * 公平鎖模式下請求讀鎖的線程是否需要排隊獲取鎖
    */
    final boolean readerShouldBlock() {
        //如果同步隊列中有排隊的線程,當前線程請求鎖就需要排隊
        return hasQueuedPredecessors();
    }
}

5.2 讀鎖lock

/**
* 阻塞式獲取共享讀鎖
*/
public void lock() {
    sync.acquireShared(1);
}

阻塞式獲取共享讀鎖的實現是通過調用ReentrantReadWriteLock的內部類Sync的acquireShared方法(繼承自AQS)實現的。

public final void acquireShared(int arg) {
    if (tryAcquireShared(arg) < 0)
        doAcquireShared(arg);
}

ReentrantReadWriteLock的內部類Sync重寫了AQS的tryAcquireShared方法,所以最終會調用Sync類的tryAcquireShared:

/**
* 非阻塞嘗試獲取讀鎖
*/
protected final int tryAcquireShared(int unused) {
    //獲取當前線程
    Thread current = Thread.currentThread();

    //獲取同步狀態
    int c = getState();

    //exclusiveCount寫鎖持有數目!=0,並且持有寫鎖的線程非當前線程,
    //讀寫互斥,return -1,表示嘗試獲取讀鎖失敗
    if (exclusiveCount(c) != 0 &&
        getExclusiveOwnerThread() != current)
        return -1;

    //走到這裏說明寫鎖沒有被持有,或者寫鎖被持有寫鎖被持有但是持有寫鎖的線程是當前線程
    //這兩種情況,都說明允許獲取讀鎖       
    int r = sharedCount(c); //獲取讀鎖持有數目

    //CAS獲取讀鎖,獲取成功return 1
    if (!readerShouldBlock() &&
        r < MAX_COUNT &&
        compareAndSetState(c, c + SHARED_UNIT)) {
        if (r == 0) {
            firstReader = current;
            firstReaderHoldCount = 1;
        } else if (firstReader == current) {
            firstReaderHoldCount++;
        } else {
            HoldCounter rh = cachedHoldCounter;
            if (rh == null || rh.tid != getThreadId(current))
                cachedHoldCounter = rh = readHolds.get();
            else if (rh.count == 0)
                readHolds.set(rh);
            rh.count++;
        }
        return 1;
    }
    //處理CAS失敗和重入讀鎖的情況,不阻塞
    return fullTryAcquireShared(current);
}

如果tryAcquireShared方法嘗試獲取鎖失敗,就會調用AQS的diAcquiredShared方法排隊獲取共享讀鎖,邏輯跟之前講的acquiredShared方法非常相似:

/**
* 嘗試獲取共享讀鎖
*/
private void doAcquireShared(int arg) {
    //等待讀鎖節點入同步隊列
    final Node node = addWaiter(Node.SHARED);

    boolean failed = true;
    try {
        boolean interrupted = false;
        //死循環等待獲取鎖,跳出循環的前提是,當前節點是head的後繼節點,並且嘗試獲取鎖成功
        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;
                }
            }
            //如果沒有獲取到鎖,就要park阻塞當前請求鎖的線程
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

跟acquireQueued的區別就在於嘗試獲取鎖的邏輯和獲取到鎖的後續操作。

5.3 寫鎖釋放unlock

/**
* 釋放獨佔寫鎖
*/
public void unlock() {
    //調用AQS的release方法
    sync.release(1);
}
/**
* AQS釋放獨佔鎖,該方法在ReentrantLock和ReentrantReadWriteLock都有使用
*/
public final boolean release(int arg) {
    if (tryRelease(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}
/**
* ReentrantReadWriteLock內部類Sync重寫tryRelease方法
*/
protected final boolean tryRelease(int releases) {
    //如果當前請求釋放寫鎖的線程不是持有寫鎖的線程,拋異常
    if (!isHeldExclusively())
        throw new IllegalMonitorStateException();
    //釋放後鎖的持有數量,因爲寫鎖在低16位,所以直接減就可以,不用移位
    int nextc = getState() - releases;

    boolean free = exclusiveCount(nextc) == 0;
    //如果寫鎖持有數量爲0,說明寫鎖已經被完全釋放
    //講寫鎖持有線程設置爲null
    if (free)
        setExclusiveOwnerThread(null);
    //這裏鎖還沒有釋放,直接通過set方法設置同步狀態
    setState(nextc);
    return free;
}

5.3 讀鎖釋放unlock

/**
* ReentrantReadWriteLock釋放共享讀鎖
*/
public void unlock() {
    //直接調用AQS的releaseShared方法釋放共享鎖
    sync.releaseShared(1);
}
/**
* 釋放共享鎖
*/
protected final boolean tryReleaseShared(int unused) {
   Thread current = Thread.currentThread();
   if (firstReader == current) {
       // assert firstReaderHoldCount > 0;
       if (firstReaderHoldCount == 1)//如果是首次獲取讀鎖,那麼第一次獲取讀鎖釋放後就爲空了
           firstReader = null;
       else
           firstReaderHoldCount--;
   } else {
       HoldCounter rh = cachedHoldCounter;
       if (rh == null || rh.tid != getThreadId(current))
           rh = readHolds.get();
       int count = rh.count;
       if (count <= 1) { //表示全部釋放完畢
           readHolds.remove();  //釋放完畢,那麼久把保存的記錄次數remove掉
           if (count <= 0)
               throw unmatchedUnlockException();
       }
       --rh.count;
   }
   for (;;) {
       int c = getState();
        // nextc 是 state 高 16 位減 1 後的值
       int nextc = c - SHARED_UNIT;
       if (compareAndSetState(c, nextc)) //CAS設置狀態
           return nextc == 0; //這個判斷如果高16位減1後的值==0,那麼就是讀狀態和寫狀態都釋放了
   }
}

關於ReentrantReadWriteLock實現分析,就先講到這裏。其它比如tryLock、lockInterruptibly就不一一講解了,實現思想跟ReentrantLock類似,只不過ReentrantReadWriteLock對同步狀態分爲高16位和低16位分別處理。同時通過代碼分析可以知道,無論是ReentrantLock還是ReentrantReadWriteLock都最大程度上覆用了AQS提供的方法,可以說設計非常精巧。除了ReentrantLock和ReentrantReadWriteLock,像之前文章介紹的同步工具Semaphore也使用了AQS,可以講AQS是JUC的基礎。

5.4 讀寫鎖同步狀態分析

上面講到讀寫鎖ReentrantReadWriteLock中,將同步狀態分爲高16位和低16位,分別表示讀鎖和寫鎖,讀鎖位共享鎖寫鎖位獨佔鎖,從而實現讀寫鎖分離。這裏我們就來介紹一下ReentrantReadWriteLock是如何實現實現分別維護讀寫鎖狀態的。

static final int SHARED_SHIFT   = 16;

//65536
static final int SHARED_UNIT    = (1 << SHARED_SHIFT);
        
//65535
static final int MAX_COUNT      = (1 << SHARED_SHIFT) - 1;
        
//65535
static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;

/** 獲取讀的狀態  */
static int sharedCount(int c)    { return c >>> SHARED_SHIFT; }
        
/** 獲取寫鎖的獲取狀態 */
static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }

我們按照圖示內容的數據進行運算,圖示的32位二進制數據爲:00000000000000100000000000000011

  • 讀狀態獲取

00000000000000100000000000000011 >>> 16,無符號右移16位(獲取高16位值),結果如下:00000000000000000000000000000010,換算成10進制數等於2,說明讀狀態爲:2。

  • 寫狀態獲取

00000000000000100000000000000011 & 65535,轉換成2進制運算爲00000000000000100000000000000011 & 00000000000000001111111111111111

這裏其實就是獲取低16位值,運算結果爲: 00000000000000100000000000000011 ,換算成10進製爲3。

這裏的設計非常巧妙,在不修改AQS的代碼前提下,僅僅通過原來的State變量就滿足了讀鎖和寫鎖的分離。

參考鏈接:

1. Java API

2. 《Java編程的邏輯》

3. 【死磕Java併發】—–J.U.C之AQS(一篇就夠了)

4. Java鎖之ReentrantReadWriteLock

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章