【JUC】從Lock到AQS瞭解Java中的鎖

從Lock到AQS瞭解Java中的鎖

本文主要會從 Lock 接口到 AQS 抽象類的 API 以及源碼分析 Java 中鎖的實現,通過演示相關組件的代碼 Demo 瞭解其使用,以及瞭解如何通過 AQS 實現一個鎖。

Lock Interface

鎖是我們在開發問題中解決併發問題的常見手段,而 Lock 接口及其實現子類可能是我們工作中經常涉及到的一種保證同步的一種方式,那 Lock 是什麼呢?

Lock implementations provide more extensive locking operations than can be obtained using synchronized methods and statements. They allow more flexible structuring, may have quite different properties, and may support multiple associated Condition objects.

我們知道最基礎的線程同步方式可以使用 synchronized 去修飾需要同步的代碼塊,並且隨着 JDK 1.6 之後引入了鎖膨脹的機制,在某些情況下也並不是一個重量級鎖,但是相較於 Lock 接口的實現子類,它卻顯得不是很靈活。而 Lock 接口的實現子類最大的優勢就是使用起來十分靈活且便於擴展。我們首先來看下 Lock 接口的定義:

public interface Lock {
    
    // Acquires the lock.
    void lock();
    
    // Acquires the lock unless the current thread is interrupted.
    void lockInterruptibly();
    
    // Acquires the lock only if it is free at the time of invocation.
    boolean tryLock();
    
    // Acquires the lock if it is free within the given waiting time
    // and current thread has not been interrupt
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
    
    // Releases the lock
    void unlock();
    
    // Return a new Condition instance that is bound to this instance.
    Condition newCondition();
}

接口的定義很簡單,我們先不去了解最後的 newCondition() 函數,其他幾個函數的定義我們見名知義,分別就是獲取鎖以及釋放鎖。

我們可以來看一下實現 Lock 接口的類,較爲常見的有以下幾個:

  • ReentrantLock:重入鎖,實現了 Lock 接口。重入是指線程在獲取鎖之後,再次獲取該鎖不需要阻塞,而是直接關聯一次計數器增加重入次數;
  • ReentrantReadWriteLock:重入讀寫鎖,實現 ReadWriteLock 接口,該類中維護了兩個鎖——ReadLock & WriteLock,它們都分別實現了 Lock 接口。其適用在讀多寫少的情況下,其基本原則是:讀和讀不互斥、讀和寫互斥、寫和寫互斥,即涉及到影響數據的操作都會存在互斥;
  • StampedLock:JDK 1.8 引入的一種新的鎖機制,可以認爲是讀寫鎖的一個改進版本。讀寫鎖通過分離讀和寫使得讀和讀之間可以進行併發操作,但是由於讀和寫還是有衝突,所以當存在大量讀線程時可能造成寫線程的飢餓。StampedLock 改用樂觀鎖思想實現讀策略,使得寫操作不會被阻塞。

接下來我們以一段 Demo 來演示 ReentrantLock 的簡單使用:

public class ReentrantLockDemo {
    
    private static int count = 0;
    static Lock lock = new ReentrantLock();
    static CountDownLatch countDownLatch = new CountDownLatch(3);

    public static void incr() {
        // acquires the lock
        lock.lock();
        try {
            Thread.sleep(1);
            countDownLatch.countDown();
            count++;
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            // release
            lock.unlock();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 3; i++) {
            new Thread(ReentrantLockDemo::incr).start();
        }
        countDownLatch.await();
        System.out.println(count);
    }
}

上面這段代碼通過 ReentrantLock 實現了incr()中 count++ 做遞增的這一操作,如果這一函數沒有實現同步,那麼一定存在線程安全問題。所以用到了 ReentrantLock 來作爲一個同步鎖實現了線程安全,在進行 count++ 操作之前進行加鎖,之後再在 finally 中釋放鎖。

而 Lock 接口的功能實現基本上都是通過聚合一個同步器的內部子類來完成線程的訪問控制的,接下來我們就來了解這個同步器,也就是 AQS。

AQS

我們需要先考慮一個問題,多個線程併發爭奪鎖資源,那麼 Lock 接口的實現類是如何保證競爭失敗的線程的等待以及後續的喚醒呢?帶着這個問題,我們來了解一下什麼是 AQS。

什麼是 AQS

AQS (Abstract Queued Synchronizer),它提供了一個 FIFO 隊列來使得資源獲取線程的排隊工作,同時使用了一個 int 成員變量表示同步狀態。其本身沒有實現任何的同步接口,經常被作爲其他實現了 Lock 接口的類用來實現同步鎖以及涉及到其他同步功能的核心組件,類似於上述所提及到的 ReentrantLock,它便利用了 AQS。所以瞭解 AQS 是瞭解 JUC 必不可少的一部分知識。

Doug Lea 在設計 AQS 時,期望其能成爲實現大部分同步需求的基礎,所以 AQS 的設計也是基於”模板方法模式“的。那麼對於繼承了 AQS 的類來說,需要去重寫指定的方法以實現我們需要的功能,之後再由組合了 AQS 的同步組件去調用。

我們來看 AQS 提供的可重寫的方法:

protected boolean tryAcquire(int arg) : 獨佔式獲取同步狀態,實現該方法需要查詢當前狀態並判斷同步狀態是否符合預期,然後再進行 CAS 設置同步狀態;

protected boolean tryRelease(int arg) : 獨佔式釋放同步狀態,等待獲取同步狀態的線程將有機會獲取同步狀態;

protected int tryAcquireShared(int arg) : 共享式獲取同步鎖,返回大於等於 0 的值,表示獲取成功,反之,獲取失敗;

protected boolean tryReleaseShared(int arg) : 共享式釋放同步狀態;

protected boolean isHeldExclusively() : 當前同步鎖是否在獨佔模式下被線程佔用,一般該方法表示是否被當前線程所佔。

從使用層面上來說,AQS 的功能分爲兩種:獨佔和共享

  • 獨佔鎖,每次只能有一個線程持有鎖,比如 ReentrantLock 就是以獨佔的方式實現互斥鎖;
  • 共享鎖,允許多個線程同時獲取鎖,併發訪問共享資源,比如 ReentrantReadWriteLock;

除此之外,重寫同步方法時,需要使用同步器提供的如下三個方法來訪問或修改同步狀態。

  • getState() : 獲取當前同步狀態;
  • setState(int newState) : 設置當前同步狀態;
  • compareAndSetState(int expect, int update) : 使用 CAS 設置當前狀態,該方法能夠保證狀態設置的原子性。

接下來,我們就以文檔中提供的示例代碼做說明:

public class Mutex implements Lock, Serializable {

    // 靜態內部類,繼承 AQS 並重寫其中方法
    private static class Sync extends AbstractQueuedSynchronizer {
        // Reports whether in locked state
        @Override
        protected boolean isHeldExclusively() {
            return getState() == 1;
        }

        // Acquire the lock if state is zero
        public boolean tryAcquire(int acquires) {
            assert acquires == 1; // Otherwise unused
            if (compareAndSetState(0, 1)) { // 通過 CAS 設置
                setExclusiveOwnerThread(Thread.currentThread());
                return true;
            }
            return false;
        }

        // Release the lock by setting state to zero
        protected boolean tryRelease(int releases) {
            assert releases == 1; // Otherwise Unused
            if (getState() == 0) {
                throw new IllegalMonitorStateException();
            }
            setExclusiveOwnerThread(null);
            setState(0);
            return true;
        }

        // Providers a Condition
        Condition newCondition() {
            return new ConditionObject();
        }

        // Deserializes properly
        private void readObject(ObjectInputStream s) throws IOException, ClassNotFoundException {
            s.defaultReadObject();
            setState(0); // reset to unlocked state
        }
    }
    
    // 將操作代理到 Sync 上
    private final Sync sync = new Sync();

    public void lock() { sync.acquire(1); }
    public boolean tryLock() { return sync.tryAcquire(1); }
    public void unlock() { sync.release(1); }
    public Condition newCondition() { return sync.newCondition(); }
    public boolean isLocked() { return sync.isHeldExclusively();}
    public boolean hasQueuedThreads() { return sync.hasQueuedThreads();}
    public void lockInterruptibly() throws InterruptedException { 
        sync.acquireInterruptibly(1); 
    }
    public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException {
        return sync.tryAcquireNanos(1, unit.toNanos(timeout));
    }
}

這樣一個簡單的獨佔鎖就實現了。外部使用 Mutex 時並不會感知到 AQS 的存在,而是直接調用 Mutex 所提供的方法。而我們在實現 Mutex 時,也並不需要去關注如何實現釋放鎖,獲取鎖的邏輯,只需要將其委託給 AQS 的實現類即可,這便降低了實現一個可靠自定義同步組件的門檻。

AQS 的實現原理

知道如何依靠 AQS 實現一個同步組件後,我們再回到我們最先提出的問題。Lock 接口的實現類,或者說 AQS 如何保證競爭失敗的線程的等待以及後續的喚醒呢?

AQS 的實現依賴內部的同步隊列,也就是 FIFO 的雙向隊列,如果當前線程競爭鎖失敗,那麼 AQS 會把當前線程以及等待狀態信息構造成一個 Node 節點並加入到同步隊列中,同時再阻塞該線程。當獲取鎖的線程釋放後,會從隊列中喚醒一個阻塞節點(線程)。

我們先來了解構造出來的 Node 的結構:

static final class Node {

    static final Node SHARED = new Node(); // 共享
    static final Node EXCLUSIVE = null; // 獨佔
    
    static final int CANCELLED =  1;
    static final int SIGNAL    = -1;
    static final int CONDITION = -2;
    static final int PROPAGATE = -3;
    volatile int waitStatus; // 當前節點在隊列中的狀態

    volatile Node prev; // 前驅節點
    volatile Node next; // 後繼節點

    volatile Thread thread; // 當前節點上的隊列

    Node nextWaiter; //存儲在condition隊列中的後繼節點

    // 是否爲共享鎖
    final boolean isShared() {
        return nextWaiter = SHARED;
    }

    final Node predecessor() throws NullPointerException {
        Node p = prev;
        if (p == null)
            throw new NullPointException();
        else
            return p;
    }

    Node() { // Used to establish inital head or SHARED marker
    }

    // 將線程構造成一個 Node,添加等待隊列
    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 提供的一個基於 CAS 的設置尾結點的方法:compareAndSetTail(Node expect, Node update),這個過程如下所示:

這一部分的代碼邏輯如下:

for (;;) {
    Node t = tail;
    if (t == null) { // 沒有尾結點
        if (compareAndSetHead(new Node()))
            tail = head;
    } else {
        node.prev = t;
        if (compareAndSetTail(t, node)) { // CAS 替換尾結點
            t.next = node; // 將先前的尾結點的後置節點設置爲現尾結點
            return t;
        }
    }
}

有線程獲取鎖,那麼同樣的也會有線程釋放鎖。在 AQS 中,head 節點表示獲取同步狀態成功的節點,當 head 節點在釋放同步狀態時,會喚醒後繼節點,如果後繼節點獲取成功,那麼會將自己設置爲頭節點,這一步驟由於只有一個線程能夠成功得獲取到同步狀態,所以並不需要使用 CAS 來保證,只需將首結點設置爲原首節點的後繼節點並斷開原首節點的 next 引用即可。

AQS 源碼分析

從上面瞭解完什麼是 AQS 以及 AQS 的最基本原理,我們再來通過 AQS 分析 AQS 的具體執行邏輯。我們從 ReentrantLock 入手,ReentrantLock 中公平鎖和非公平鎖在底層的實現原理是相通(後面會舉例),所以這裏我們用非公平鎖舉例,源碼之間的調用過程我們利用時序圖的方式來展現:

鎖的獲取

由上圖可知,如果當鎖調用失敗時,會調用 addWaiter() 方法將當前線程封裝成 Node 節點加入到 AQS 隊列,那我們就以這個邏輯看下從 ReentrantLock 到 addWaiter() 方法中的實現:

java.util.concurrent.locks.ReentrantLock

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

這裏是獲取鎖的入口,調用了 sync 這個類裏面的方法,這裏的 sync 和我們上面演示的代碼一樣,也是一個用來幫助實現同步組件的靜態類:

abstract static class Sync extends AbstractQueuedSynchronizer {
    
    // ...
    
    abstract void lock();
	
    // ...
}

Sync 是一個抽象類,它有兩個具體的實現類,分別是 NofairSync(非公平鎖),FairSync(公平鎖):

  • 公平鎖:表示所有的線程嚴格按照 FIFO 來獲取鎖;
  • 非公平鎖:表示可以存在搶佔鎖的功能,也就是說不管當前節點之前是否存在其他線程等待,當前節點都有機會搶佔鎖。

我們來看下 NofairSync 對 Sync 中 Lock 的實現:

static final class NonfairSync extends Sync {
    // ...
    final void lock() {
        if (compareAndSetState(0, 1)) // 通過 CAS 操作來修改 state 狀態
            setExclusiveOwnerThread(Thread.currentThread()); // 爭搶成功則修改獲得鎖狀態的線程
        else
            acquire(1);
    }
}

在講解這段代碼之前,我們需要先來了解 state, 在 AQS 中,利用 state 來表示鎖的狀態(不同的 AQS 實現,state 所表達的含義是不一樣的),這裏以 ReentrantLock 舉例:

  • state = 0:表示無鎖狀態;
  • state > 0:表示已有線程獲得了鎖,如果只有一個線程獲得了鎖,此時 state 可能爲 1,但是 ReentrantLock 允許重入,所以當同一線程多次獲取到同步鎖時,state 會遞增,比如重入了 5 次,那麼 state = 5。而在釋放鎖的時候,也需要等到 state 遞減到 0 的時候才能釋放鎖。

回到上面這段代碼,這裏的主要邏輯是:當調用 lock 方法時,想通過 CAS 去搶佔鎖(即修改 state 的狀態),如果搶佔失敗,將獲得鎖狀態的線程設置爲當前線程,如果搶佔失敗,那麼就通過調用 acquire() 來繼續下面的邏輯。

java.util.concurrent.locks.AbstractQueuedSynchronizer

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

acquire 是 AQS 中的方法,如果 CAS 操作沒有成功,說明當前鎖已經被有線程佔有了,acquire 中的邏輯主要是:

  • 通過 tryAcquire 嘗試獲取獨佔鎖,如果成功返回 true,失敗返回 false;
  • 如果 tryAcquire 失敗,則會通過 addWaiter 方法將當前線程封裝成 Node 添加到 AQS 隊列尾部;
  • acquireQueued,將 Node 作爲參數,通過自旋去嘗試獲取鎖。

接下來就依次介紹這兩個方法,tryAcquire,這個方法的作用是嘗試獲取鎖,如果成功返回 true,不成功返回 false。

protected final boolean tryAcquire(int acquires) {
    return nonfairTryAcquire(acquires);
}
final boolean nonfairTryAcquire(int acquires) {
    // 獲得當前執行的線程
    final Thread current = Thread.currentThread();
    // 獲取 state 的值
    int c = getState();
    if (c == 0) { // state=0 說明當前是無鎖狀態
        // 通過 CAS 操作來替換 state 的值改爲1
        if (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;
}

這兩部分的代碼的邏輯:

  • 獲取當前線程,判斷當前的鎖的狀態;
  • 如果 state = 0 表示當前是無鎖狀態,通過 CAS 更新 state 狀態的值;
  • 如果當前線程是輸屬於重入,則增加重入次數。

而當 tryAcquire 方法獲取鎖失敗以後,則會先調用 addWaiter 將當前線程封裝成 Node,然後添加到 AQS 隊列:

private Node addWaiter(Node mode) { // mode=Node.EXCLUSIVE
    //將當前線程封裝成Node,並且mode爲獨佔鎖
    Node node = new Node(Thread.currentThread(), mode); 
    // Try the fast path of enq; backup to full enq on failure
    // tail是AQS的中表示同步隊列隊尾的屬性,剛開始爲null,所以進行enq(node)方法
    Node pred = tail;
    if (pred != null) { //tail不爲空的情況,說明隊列中存在節點數據
        node.prev = pred;  //講當前線程的Node的prev節點指向tail
        if (compareAndSetTail(pred, node)) {//通過cas講node添加到AQS隊列
            pred.next = node;//cas成功,把舊的tail的next指針指向新的tail
            return node;
        }
    }
    enq(node); //tail=null,將node添加到同步隊列中
    return node;
}

該方法的主要邏輯如下:

  • 將當前線程封裝成 Node;
  • 判斷當前鏈表中的 tail 節點是否爲空,如果不爲空,則通過 CAS 操作把當前線程的 Node 添加到 AQS 隊列
  • 如果爲空或者 CAS 失敗,則調用 enq(final Node node) 將節點添加到 AQS 隊列;
private Node enq(final Node node) {
    // 自旋,知道成功將節點添加到隊列
    for (;;) {
        Node t = tail; // 如果是第一次添加到隊列,那麼tail=null
        if (t == null) { // Must initialize
            //CAS的方式創建一個空的Node作爲頭結點
            if (compareAndSetHead(new Node()))
                //此時隊列中只一個頭結點,所以tail也指向它
                tail = head;
        } else {
            //進行第二次循環時,tail不爲null,進入else區域。將當前線程的Node結點的prev指向tail,
            // 然後使用CAS將tail指向Node
            node.prev = t;
            if (compareAndSetTail(t, node)) {
                // t此時指向tail,所以可以CAS成功,將tail重新指向Node。此時t爲更新前的tail的值,即指向空的頭結點,
                // t.next=node,就將頭結點的後續結點指向Node,返回頭結點
                t.next = node;
                return t;
            }
        }
    }
}

至此,就能成功將沒有搶佔到鎖的線程封裝到 Node 中並加入到 AQS 隊列上。接下來我們來了解 acquireQueued 方法的主要邏輯,在這裏面會做搶佔鎖的操作:

final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {
            final Node p = node.predecessor();// 獲取prev節點,若爲null即刻拋出NullPointException
            if (p == head && tryAcquire(arg)) {// 如果前驅爲head纔有資格進行鎖的搶奪
                setHead(node); // 獲取鎖成功後就不需要再進行同步操作了,獲取鎖成功的線程作爲新的head節點
                // 凡是head節點,head.thread與head.prev永遠爲null, 但是head.next不爲null
                p.next = null; // help GC
                failed = false; //獲取鎖成功
                return interrupted;
            }
            // 如果獲取鎖失敗,則根據節點的waitStatus決定是否需要掛起線程
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt()) // 若前面爲true,則執行掛起,待下次喚醒的時候檢測中斷的標誌
                interrupted = true;
        }
    } finally {
        if (failed) // 如果拋出異常則取消鎖的獲取,進行出隊(sync queue)操作
            cancelAcquire(node);
    }
}
  • 獲取當前節點的 prev 節點;
  • 如果 prev 節點爲 head 節點,那麼它就有資格去爭搶鎖,調用 tryAcquire 搶佔鎖;
  • 搶佔鎖成功以後,把獲得鎖的節點設置爲 head,並且移除原來的初始化 head 節點;
  • 如果獲取鎖失敗,則根據 waitStatus 決定是否需要掛起線程;
  • 最後,通過 cancelAcquire 取消獲取鎖的操作;

前面的邏輯都很好理解,主要看一下shouldParkAfterFailedAcquire這個方法和parkAndCheckInterrupt的作用:

從上面代碼的分析可以看出,只有隊列的第二個節點纔可以有機會徵用鎖,如果成功獲取鎖,則此節點晉升爲頭節點。對於第三個及以後的節點,if (p == head)條件不成立,首先進行shouldParkAfterFailedAcquire(p, node) 操作。

shouldParkAfterFailedAcquire 方法判斷一個爭用鎖的線程是否應該被阻塞。它首先判斷一個節點的前置節點的狀態是否爲 Node.SIGNAL,如果是,說明前置節點已經將狀態設置爲”如果鎖釋放,則應當通知它,所以它可以安全地阻塞了“,返回true。

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    int ws = pred.waitStatus; //前繼節點的狀態
    if (ws == Node.SIGNAL)//如果是SIGNAL狀態,意味着當前線程需要被unpark喚醒
        return true;
    // 如果前節點的狀態大於0,即爲CANCELLED狀態時,則會從前節點開始逐步循環找到一個沒有被“CANCELLED”節點設置爲當前節點的前節點,返回false。
    // 在下次循環執行shouldParkAfterFailedAcquire時,返回true。這個操作實際是把隊列中CANCELLED的節點剔除掉。
    if (ws > 0) {// 如果前繼節點是“取消”狀態,則設置 “當前節點”的 “當前前繼節點” 爲 “‘原前繼節點'的前繼節點”。

        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else { // 如果前繼節點爲“0”或者“共享鎖”狀態,則設置前繼節點爲SIGNAL狀態。
        /*
         * waitStatus must be 0 or PROPAGATE.  Indicate that we
         * need a signal, but don't park yet.  Caller will need to
         * retry to make sure it cannot acquire before parking.
         */
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}

如果shouldParkAfterFailedAcquire返回了true,則會執行:parkAndCheckInterrupt()方法,它是通過LockSupport.park(this)將當前線程掛起到WATING狀態,它需要等待一箇中斷、unpark方法來喚醒它,通過這樣一種FIFO的機制的等待,來實現了Lock的操作。

private final boolean parkAndCheckInterrupt() {
    LockSupport.park(this);
    return Thread.interrupted();
}

LockSupport類是Java6引入的一個類,提供了基本的線程同步原語。LockSupport實際上是調用了Unsafe類裏的函數,歸結到Unsafe裏,只有兩個函數:

public native void unpark(Thread jthread);  
public native void park(boolean isAbsolute, long time);  

unpark 函數爲線程提供 “許可(permit)”,線程調用park函數則等待“許可”。這個有點像信號量,但是這個“許可”是不能疊加的,“許可”是一次性的。
permit 相當於 0/1 的開關,默認是 0,調用一次 unpark 就加 1 變成了 1。調用一次 park 會消費 permit,又會變成0。 如果再調用一次 park 會阻塞,因爲permit 已經是0了。直到permit變成 1。這時調用 unpark 會把 permit 設置爲 1。每個線程都有一個相關的 permit,permit 最多隻有一個,重複調用 unpark 不會累積。

鎖的釋放

加鎖的過程分析完以後,再來分析一下釋放鎖的過程,釋放鎖需要調用 release 方法,這個方法裏面做兩件事:1、釋放鎖 ;2、喚醒park的線程

public final boolean release(int arg) {
    if (tryRelease(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}

tryRelease 這個動作可以認爲就是一個設置鎖狀態的操作,而且是將狀態剪掉傳入的參數值(參數是1),如果結果狀態爲0,就將排它鎖的 Owner 設置爲null,以使得其它的線程有機會進行執行。

在排它鎖中,加鎖的時候狀態會增加1(當然可以自己修改這個值),在解鎖的時候減掉1,同一個鎖,在可以重入後,可能會被疊加爲2、3、4這些值,只有unlock() 的次數與 lock() 的次數對應纔會將 Owner 線程設置爲空,而且也只有這種情況下才會返回 true。

protected final boolean tryRelease(int releases) {
    int c = getState() - releases; // 這裏是將鎖的數量減1
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    boolean free = false;
    if (c == 0) { 
        // 由於重入的關係,不是每次釋放鎖c都等於0
        // 直到最後一次釋放鎖時,纔會把當前線程釋放
        free = true;
        setExclusiveOwnerThread(null);
    }
    setState(c);
    return free;
}

在方法 unparkSuccessor(Node) 中,就意味着着真正要釋放鎖了,它傳入的是head節點(head節點是佔用鎖的節點),當前線程被釋放之後,需要喚醒下一個節點的線程:

private void unparkSuccessor(Node node) {
    int ws = node.waitStatus;
    if (ws < 0)
        compareAndSetWaitStatus(node, ws, 0);
    Node s = node.next;
    if (s == null || s.waitStatus > 0) { //判斷後繼節點是否爲空或者是否是取消狀態,
        s = null;
        for (Node t = tail; t != null && t != node; t = t.prev)
            //然後從隊列尾部向前遍歷找到最前面的一個waitStatus小於0的節點
            //至於爲什麼從尾部開始向前遍歷
            //因爲在 doAcquireInterruptibly.cancelAcquire 方法的處理過程中只設置了next的變化,沒有設置prev的變化
            //在最後有這樣一行代碼:node.next = node
            //如果這時執行了unparkSuccessor方法,並且向後遍歷的話,就成了死循環了,所以這時只有 prev 是穩定的
            if (t.waitStatus <= 0)
                s = t;
    }
    //內部首先會發生的動作是獲取head節點的next節點
    //如果獲取到的節點不爲空,則直接通過:“LockSupport.unpark()”方法來釋放對應的被掛起的線程
    //這樣一來將會有一個節點喚醒後繼續進入循環進一步嘗試tryAcquire()方法來獲取鎖
    if (s != null)
        LockSupport.unpark(s.thread); //釋放許可
}

參考資料

  • Lea D. The java. util. concurrent synchronizer framework[J]. Science of Computer Programming, 2005, 58(3): 293-309.

  • 《Java併發編程的藝術》

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