Java併發4:鎖

mortal同學發表於2018-12-28

Lock介面

在Lock介面出現之前,Java程式依靠 synchronized 關鍵字實現鎖的功能。鎖提供了類似的同步功能,只是在使用時需要顯式獲取和釋放鎖,同時還擁有了鎖的獲取釋放的操作性、可中斷的獲取鎖以及超時獲取鎖等多種 synchronized 不具備的同步特性。

Lock介面提供的 synchronized 介面不具備的特性:

  • 嘗試非阻塞的獲取鎖:當前執行緒嘗試獲取鎖,如果這一時刻鎖沒有被其他執行緒獲取到,則成功獲取並持有鎖
  • 能被中斷的獲取鎖:與 synchronized 不同,獲取到鎖的執行緒能響應中斷,當獲取到鎖的執行緒被中斷,中斷異常被丟擲,同時釋放鎖
  • 超時獲取鎖:在指定的截止時間之前獲取鎖,如果時間到了仍然無法獲取鎖,返回

Lock的常用方法:

  • void lock() 獲取鎖,獲取鎖後返回
  • void lockInterruptibly throws InterruptedException可中斷的獲取鎖,在鎖的獲取中可以中斷當前執行緒
  • boolean tryLock()嘗試非阻塞的獲取鎖,呼叫後立即返回,獲取了返回true,否則返回false
  • boolean tryLock(long time,TimeUnit unit) throws InterruptedException超時獲取鎖。當前執行緒在時間內獲得了鎖,返回true;當前執行緒在時間內被中斷,丟擲異常;超出時間,返回false
  • void unlock()釋放鎖
  • Condition newCondition()獲取等待通知元件,該元件與當前的鎖繫結。當前執行緒獲取了鎖,才能呼叫該元件的wait()方法,呼叫後,當前執行緒將釋放鎖

佇列同步器AQS

佇列同步器(AbstractQueuedSynchronizer)是用來構建鎖或者其他同步元件的基礎框架,它使用了一個int成員變數表示同步狀態,通過內建的FIFO佇列完成資源獲取執行緒的排隊工作。

AQS 介面

同步器的時機是基於模板方法模式,使用者需要繼承同步器並重寫特定方法。重寫時,使用以下三個方法訪問或修改同步狀態:

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

除此之外,同步器提供了模板方法,分為三類:獨佔式獲取與釋放同步狀態,共享式獲取與釋放同步狀態,查詢同步佇列中的等待執行緒情況。

AQS實現分析

同步佇列

AQS 內部依賴一個同步的 FIFO 雙向佇列來完成同步狀態的管理。當前執行緒獲取同步狀態失敗時,將當前執行緒以及等待狀態等資訊構造成一個節點並將其加入同步佇列,同時阻塞該執行緒;當同步狀態釋放時,會把首節點中的執行緒喚醒,使其再次嘗試獲取同步狀態。

同步佇列中,一個幾點代表一個執行緒,儲存著執行緒的引用、等待狀態、前驅和後繼節點。

Java併發4:鎖

  • int waitStatus 等待狀態
  • Node prev 前驅
  • Node next 後繼
  • Node nextWaiter 等待佇列中的後繼節點
  • Thread thread 獲取同步狀態的執行緒

等待狀態包含如下:

  • CANCELLED: 在同步佇列中等待的執行緒超時或被中斷,需要從同步佇列中取消等待,節點進入該狀態將不會變化
  • SIGNAL: 後繼節點的執行緒處於等待狀態,而當前節點的執行緒如果釋放了同步狀態或者取消,將會通知後繼節點,使得後繼節點的執行緒得以執行
  • CONDITION:節點在等待佇列中,節點執行緒等待在 Condition 上,當其他執行緒對 Condition 呼叫了 signal() 方法後,該節點會從等待佇列轉移到同步佇列中
  • PROPAGATE:表示下一次共享式同步狀態獲取將無條件傳播下去
  • INITIAL:初始狀態

入隊

同步器提供了一個基於CAS的設定尾節點的方法compareAndSetTail(Node expect,Node update),傳遞的兩個引數是當前執行緒認為的尾節點和當前的節點,只有設定成功後,當前節點才正式與之前的尾節點建立關聯。

出隊

同步佇列遵循FIFO,首節點是獲取同步狀態成功的節點,首節點的執行緒在釋放同步狀態時,會喚醒後繼節點,後繼節點將會在獲取同步狀態成功時將自己設定為首節點。

獨佔式同步狀態獲取與釋放

獨佔式也就是同一時刻僅有一個執行緒持有同步狀態。

同步狀態獲取

獨佔式同步狀態獲取採用acquire(int arg)方法。該方法對中斷不敏感,由於執行緒獲取同步狀態失敗加入到同步佇列中,後序對執行緒進行中斷操作時,執行緒不會從佇列中移除。

public final void acquire(int arg) {

        if (!tryAcquire(arg) &&//獲取同步狀態
        //首先生成節點加入佇列
        //然後等待前驅節點成為頭節點並獲取同步狀態
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }
複製程式碼
  • tryAcquire:嘗試獲取鎖,獲取成功則設定鎖狀態並返回true,否則返回false
  • addWaiter:如果獲取鎖失敗,呼叫該方法將當前執行緒加入到同步佇列尾部
  • acquireQueued:當前執行緒根據公平性原則進行阻塞等待(自旋,也就是死迴圈),直到獲取鎖為止。返回當前執行緒在等待過程中有沒有中斷過。
  • selfInterrupt:產生一箇中斷
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;
            }
        }
        enq(node);
        return node;
    }
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;
                }
            }
        }
    }
複製程式碼

addWaiter(Node mode) 用來構造節點以及加入同步佇列。通過使用compareAndSetTail(Node expect,Node update)來確保節點能被執行緒安全新增。在enq(final Node node)方法中,使用死迴圈保證節點的正確新增。在死迴圈中,只有通過CAS將節點設定為尾節點之後,當前執行緒才能從該方法返回。

此時,節點進入同步佇列之後,進入了一個自選的過程。當條件滿足,獲得了鎖,退出佇列。

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);
        }
    }

複製程式碼

在該方法中,只有前驅是頭節點才能嘗試獲取同步狀態。原因有兩個:1. 頭節點是成功獲取到同步狀態的節點,頭節點執行緒釋放鎖,喚醒其後繼節點;2. 維護同步佇列的FIFO原則。

整體流程如下圖所示:

Java併發4:鎖

同步狀態釋放

當前執行緒獲取同步狀態並執行了相應邏輯後,需要釋放同步狀態,使得後續節點能繼續獲取同步狀態。呼叫release(int arg)方法釋放同步狀態,在釋放同步狀態之後,會喚醒其後繼節點。

public final boolean release(int arg) {
        if (tryRelease(arg)) {//釋放鎖
            Node h = head;
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);//喚醒頭節點的後繼節點
            return true;
        }
        return false;
    }
複製程式碼

總結:獲取同步狀態時,維護一個同步佇列,獲取狀態失敗的執行緒加入佇列並進行自旋;移出佇列的條件是前驅為頭節點且成功獲取同步狀態。釋放時,先釋放同步狀態,然後喚醒頭節點的後繼節點。

共享式同步狀態獲取與釋放

共享式與獨佔式的最主要區別在於同一時刻獨佔式只能有一個執行緒獲取同步狀態,而共享式在同一時刻可以有多個執行緒獲取同步狀態。例如讀操作可以有多個執行緒同時進行,而寫操作同一時刻只能有一個執行緒進行寫操作,其他操作都會被阻塞。

同步狀態獲取

AQS提供了acquireShared(int arg)共享式獲取同步狀態

public final void acquireShared(int arg) {
        if (tryAcquireShared(arg) < 0)
            doAcquireShared(arg);
    }
private void doAcquireShared(int arg) {
        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;
                    }
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }
複製程式碼

acquireShared(int arg)方法中,首先呼叫tryAcquireShared(int arg)嘗試獲取同步狀態,返回一個int,當返回值大於等於0,表示能獲取到同步狀態。否則,獲取失敗呼叫doAcquireShared(int arg)自旋獲取同步狀態。

同步狀態釋放

public final boolean releaseShared(int arg) {
        if (tryReleaseShared(arg)) {
            doReleaseShared();
            return true;
        }
        return false;
    }
複製程式碼

該方法釋放同步狀態後,將會喚醒後續處於等待狀態的節點。因為可能會存在多個執行緒同時進行釋放同步狀態資源,所以需要確保同步狀態安全地成功釋放,一般都是通過CAS和迴圈來完成的。

獨佔式超時獲取同步狀態

AQS提供了tryAcquireNanos(int arg,long nanos)方法,是acquireInterruptibly(int arg)的增強。除了響應中斷之外,還有超時控制。

public final boolean tryAcquireNanos(int arg, long nanosTimeout)
            throws InterruptedException {
        if (Thread.interrupted())
            throw new InterruptedException();
        return tryAcquire(arg) ||
            doAcquireNanos(arg, nanosTimeout);
    }
複製程式碼

其中,doAcquireNanos(arg, nanosTimeout)用來超時獲取同步狀態。

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();
                //超時返回
                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納秒,如果沒有獲取到,將返回。

Java併發4:鎖

可重入鎖ReentrantLock

可重入鎖也就是支援重新進入的鎖,它表示該鎖能支援一個執行緒對資源的重複加鎖。它可以等同於 synchronized 的使用(synchronized 隱式支援重入),但是提供了比 synchronized 更強大更靈活的鎖機制,可以減少死鎖發生的概率。

ReentrantLock 還提供了公平鎖和非公平鎖的選擇,構造方法中接受一個可選的公平引數,預設是非公平的。公平鎖也就是等待時間最長的執行緒最優先獲取鎖。但是公平鎖的效率往往沒有非公平的概率高。

ReentrantLock 有一個內部類 Sync,Sync 繼承AQS,有兩個子類:公平鎖 FairSync 和非公平鎖 NonfairSync。

獲取鎖

final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            //獲取同步狀態
            int c = getState();
            //鎖處於空閒狀態
            if (c == 0) {
            //獲取鎖成功,設定為當前執行緒所有
                if (compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            //判斷鎖持有的執行緒是否是當前執行緒
            //如果是持有鎖的執行緒再次請求,將同步狀態值進行增加並返回true
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }
複製程式碼

首先判斷同步狀態,如果為0說明還沒有被執行緒持有,通過CAS獲取同步狀態,如果成功返回true。否則,判斷當前執行緒是否為獲取鎖的執行緒,如果是則獲取鎖,成功返回true。成功過去鎖的執行緒再次獲取鎖,並將同步狀態值增加。

釋放鎖

protected final boolean tryRelease(int releases) {
        //減掉releases
        int c = getState() - releases;
        //如果釋放的不是持有鎖的執行緒,丟擲異常
        if (Thread.currentThread() != getExclusiveOwnerThread())
            throw new IllegalMonitorStateException();
        boolean free = false;
        //state == 0 表示已經釋放完全了,其他執行緒可以獲取同步狀態了
        if (c == 0) {
            free = true;
            setExclusiveOwnerThread(null);
        }
        setState(c);
        return free;
    }
複製程式碼

將同步狀態是否為0作為最終釋放的條件,當同步狀態為0時,將佔有執行緒設定為null,並返回true,表示釋放成功。

公平鎖

如果一個鎖是公平的,那麼按照請求的絕對時間順序也就是FIFO進行鎖的獲取。公平鎖的獲取方法如下:

 protected final boolean tryAcquire(int acquires) {
        final Thread current = Thread.currentThread();
        int c = getState();
        if (c == 0) {
            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;
    }
複製程式碼

與非公平鎖獲取鎖的方法不同,兩者區別在於公平鎖在獲取同步狀態時多了一個限制條件:hasQueuedPredecessors()。該方法用來判斷當前執行緒是否位於CLH同步佇列中的第一個,是則返回true,否則返回false。

公平鎖保證了鎖的獲取按照FIFO原則,代價是進行大量的執行緒切換。非公平鎖雖然可能造成執行緒“飢餓”,但是極少的執行緒切換,保證了其更大的吞吐量。

與 synchronized 比較

  1. synchronized 是JVM實現的, ReentrantLock 是JDK實現的。
  2. 當持有鎖的執行緒長期不釋放鎖,正在等待的執行緒可以選擇放棄等待,ReentrantLock 可以中斷,synchronized 不可中斷。
  3. synchronized 是非公平鎖,ReentrantLock 預設是非公平,但是可以設定為公平鎖。
  4. ReentrantLock還提供了條件Condition,對執行緒的等待、喚醒操作更加詳細和靈活,在多個條件變數和高度競爭鎖的地方,ReentrantLock更加適合。
  5. 除非要使用 ReentrantLock 的高階功能,否則優先使用 synchronized。這是因為 synchronized 是 JVM 實現的一種鎖機制,JVM 原生地支援它,而 ReentrantLock 不是所有的 JDK 版本都支援。並且使用 synchronized 不用擔心沒有釋放鎖而導致死鎖問題,因為 JVM 會確保鎖的釋放。

讀寫鎖

ReentrantLock 是排他鎖,同一時刻只允許一個執行緒進行訪問,而讀寫鎖在同一時刻可以允許多個讀執行緒訪問,寫執行緒訪問時,其他的執行緒均被阻塞。通過讀鎖和寫鎖分離,使得併發性相比一般的排他鎖有了很大提升。

讀寫鎖的主要特性:

  1. 公平性:支援公平性鎖和非公平鎖
  2. 重進入:支援重入。讀執行緒獲取讀鎖後可以再次獲取讀鎖。寫執行緒獲取寫鎖後能再次獲取寫鎖,也能獲取讀鎖。
  3. 鎖降級:遵循獲取寫鎖,獲取讀鎖再釋放寫鎖的次序,寫鎖能降級成為讀鎖

介面示例

ReentrantReadWriteLock 實現了介面 ReadWriteLock,維護了一對相關的鎖。

public interface ReadWriteLock {
    Lock readLock();//返回讀鎖
    Lock writeLock();//返回寫鎖
}
複製程式碼

ReentrantReadWriteLock 使用了三個內部類

 /** 內部類  讀鎖 */
private final ReentrantReadWriteLock.ReadLock readerLock;
/** 內部類  寫鎖 */
private final ReentrantReadWriteLock.WriteLock writerLock;
    
/** 同步器 */
final Sync sync;
複製程式碼

構造方法如下:

/** 使用預設(非公平)的排序屬性建立一個新的 ReentrantReadWriteLock */
    public ReentrantReadWriteLock() {
        this(false);
    }

    /** 使用給定的公平策略建立一個新的 ReentrantReadWriteLock */
    public ReentrantReadWriteLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
        readerLock = new ReadLock(this);
        writerLock = new WriteLock(this);
    }
複製程式碼

讀寫鎖同樣依賴AQS實現同步功能,讀寫狀態就是其同步器的同步狀態。因此其同步器需要在同步狀態(一個整形變數)上維護多個讀執行緒和一個寫執行緒的狀態。

在 ReentrantLock 中使用一個 int 型別的 state 表示同步狀態,表示鎖被一個執行緒重複獲取的次數。讀寫鎖需要用一個變數維護多種狀態,所以採用了“按位切割使用”的方式維護這個變數。讀寫鎖將變數切分為兩個部分,高16位表示讀,低16位表示寫。分割之後,讀寫鎖通過位運算確定讀和寫各自的狀態。假設當前狀態為S,寫狀態等於 S&0X0000FFFF (抹去高16位),讀狀態等於 S>>>16 (無符號補0右移16位)。寫狀態增加1,S = S+1,讀狀態加1,S = S + ( 1 << 16 )。

寫鎖

寫鎖是一個支援重進入的排它鎖。

獲取

 protected final boolean tryAcquire(int acquires) {
        Thread current = Thread.currentThread();
        //當前同步狀態
        int c = getState();
        //寫鎖狀態
        int w = exclusiveCount(c);
        if (c != 0) {
            //c != 0 && w == 0 表示存在讀鎖
            //當前執行緒不是已經獲取寫鎖的執行緒
            if (w == 0 || current != getExclusiveOwnerThread())
                return false;
            //超出最大範圍
            if (w + exclusiveCount(acquires) > MAX_COUNT)
                throw new Error("Maximum lock count exceeded");
            setState(c + acquires);
            return true;
        }
        //是否需要阻塞
        if (writerShouldBlock() ||
                !compareAndSetState(c, c + acquires))
            return false;
        //設定獲取鎖的執行緒為當前執行緒
        setExclusiveOwnerThread(current);
        return true;
    }
複製程式碼

首先獲取同步狀態和寫鎖的同步狀態。如果存在讀鎖,或者當前執行緒不是持有寫鎖的執行緒,不能獲得寫鎖。如果能獲取寫鎖,且未超出最大範圍,則更新同步狀態並返回true。

釋放

寫鎖的釋放與 ReentrantLock 釋放類似,每次釋放減少寫狀態,當寫狀態為0表示寫鎖已經被釋放。

// WriteLock類提供了unlock()釋放寫鎖
public void unlock() {
    sync.release(1);
}

public final boolean release(int arg) {
    if (tryRelease(arg)) {//呼叫AQS方法釋放鎖
        Node h = head;
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}

複製程式碼
//這是定義在Sync類中的方法
protected final boolean tryRelease(int releases) {
    //釋放的執行緒不為鎖的持有者
    if (!isHeldExclusively())
        throw new IllegalMonitorStateException();
    int nextc = getState() - releases;
    //若寫鎖的新執行緒數為0,則將鎖的持有者設定為null
    boolean free = exclusiveCount(nextc) == 0;
    if (free)
        setExclusiveOwnerThread(null);
    setState(nextc);
    return free;
}
複製程式碼

讀鎖

讀鎖是一個支援重進入的共享鎖,能被多個執行緒同時獲取。

在寫狀態為0時,讀鎖總會被成功獲取,然後增加讀狀態。如果當前執行緒已經獲取了讀鎖,則增加讀狀態;如果獲取讀鎖時,寫鎖已經被其他執行緒獲取,則進入等待狀態。讀狀態時所有執行緒獲取讀鎖次數的總和,而每個執行緒各自獲取讀鎖的次數只能儲存在 ThreadLocal 中。

讀鎖的每次釋放均減少讀狀態。

鎖降級

鎖降級指的是寫鎖降級為讀鎖。鎖降級是指把持有的寫鎖把持住,再獲取到讀鎖,然後釋放擁有的寫鎖的過程。也就是需要遵循先獲取寫鎖、獲取讀鎖再釋放寫鎖的次序才可以稱為鎖降級。

鎖降級中讀鎖的獲取是必要的,主要是為了保證資料的可見性。如果當前執行緒A不獲取讀鎖而是直接釋放寫鎖,此刻另一執行緒B獲取了寫鎖並修改了資料,那麼當前執行緒A無法感知執行緒B的資料更新。如果當前執行緒A遵循鎖降級的規則,則執行緒B會被阻塞,直到當前執行緒A使用資料並釋放讀鎖之後,執行緒B才能獲取寫鎖進行資料更新。

Condition 介面

在 synchronized 控制同步時,配合 Object 的 wait(), notify(), notifyAll() 等方法實現等待/通知模式。Lock 提供了條件 Condition 介面,兩者配合實現了等待/通知模式。

Java併發4:鎖

Condition使用

Condition 定義了等待/通知兩種型別的方法,執行緒呼叫這些方法時,需要提前獲取 Condition 關聯的鎖。Condition 物件是由 Lock 物件建立出來的。

public class ConditionCase {
    Lock lock=new ReentrantLock();
    Condition condition=lock.newCondition();

    public void conditionWait() throws InterruptedException {
        lock.lock();
        try {
            condition.await();
        } finally {
            lock.unlock();
        }
    }

    public void conditionSignal(){
        lock.lock();
        try{
            condition.signal();
        }finally {
            lock.unlock();
        }
    }
}
複製程式碼

將 Condition 作為成員變數,呼叫 await() 方法造成當前執行緒在接到訊號或被中斷之前一直處於等待狀態。呼叫signal()或者signalAll()方法會喚醒一個或所有的等待執行緒,能夠從等待方法返回的執行緒必須獲取與 Condition 相關的鎖。

Condition的實現

通過 Lock 的newCondition()方法獲取 Condition。Condition 是一個介面,其為一個的實現類是 ConditionObject,且是同步器AQS的內部類。

等待佇列

每個 ConditionObject 包含一個FIFO的佇列,在佇列中的每個節點都包含了一個執行緒引用,該執行緒就是在 Condition 物件上等待的執行緒。當一個執行緒呼叫 await()方法,那麼該執行緒將釋放鎖,構造成節點加入等待佇列並進入等待狀態。此處的節點依然是AQS的內部類 Node。

public class ConditionObject implements Condition, java.io.Serializable {
    private static final long serialVersionUID = 1173984872572414699L;

    //頭節點
    private transient Node firstWaiter;
    //尾節點
    private transient Node lastWaiter;

    public ConditionObject() {
    }

    /** 省略方法 **/
}
複製程式碼

Condition 擁有頭節點和尾節點的引用。當一個執行緒呼叫await()方法,將該執行緒構造成一個幾點,加入佇列尾部。

在 Object 的監視器模型上,一個物件擁有一個同步佇列和一個等待佇列;並罰保中的同步器擁有一個同步佇列和多個等待佇列,每個 Condition 對應一個等待佇列。

等待

呼叫 Condition 的await()系列方法將使當前執行緒釋放鎖並進入等待狀態。當從該方法返回時,當前執行緒一定獲取了 Condition 相關的鎖。從佇列的角度看,當呼叫該方法時,相當於同步佇列的首節點(也就是獲取了鎖的節點)移動到 Condition 的等待佇列中。

public final void await() throws InterruptedException {
            if (Thread.interrupted())
                throw new InterruptedException();
            //將當前執行緒加入等待佇列
            Node node = addConditionWaiter();
            //釋放鎖
            int savedState = fullyRelease(node);
            int interruptMode = 0;
            //檢查該節點的執行緒是否在同步佇列上,如果不在,還不具備競爭鎖的資格,繼續等待
            while (!isOnSyncQueue(node)) {
                //掛起執行緒
                LockSupport.park(this);
                //執行緒已經中斷則退出
                if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
                    break;
            }
            //競爭同步狀態
            if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
                interruptMode = REINTERRUPT;
            if (node.nextWaiter != null) // clean up if cancelled
                unlinkCancelledWaiters();
            if (interruptMode != 0)
                reportInterruptAfterWait(interruptMode);
        }
複製程式碼

呼叫該方法的執行緒是同步佇列中的首節點(獲取鎖的執行緒)。該方法將當前執行緒構造成節點加入等待佇列,釋放同步狀態,喚醒後繼節點,然後當前執行緒進入等待狀態。然後不斷監測該節點代表的執行緒是否出現在同步佇列中(也就是收到了signal訊號),如果不是,則掛起;否則開始競爭同步狀態。

通知

呼叫 Condition 的 signal()方法,將會喚醒在等待佇列的首節點。在喚醒節點之前,將節點移入同步佇列中。

  public final void signal() {
        //檢測當前執行緒是否為擁有鎖的獨
        if (!isHeldExclusively())
            throw new IllegalMonitorStateException();
        //頭節點,喚醒條件佇列中的第一個節點
        Node first = firstWaiter;
        if (first != null)
            doSignal(first);    //喚醒
    }
複製程式碼

該方法首先會判斷當前執行緒是否已經獲取了鎖,然後喚醒等待佇列中的頭節點,具體來說,先將條件佇列中的頭節點移出,然後呼叫AQS的enq(Node node)方法將其安全地移到CLH同步佇列中。當節點移動到同步佇列中,當前執行緒再喚醒該節點的執行緒。

總結

一個執行緒獲取鎖後,通過呼叫Condition的await()方法,會將當前執行緒先加入到條件佇列中,然後釋放鎖,最後通過isOnSyncQueue(Node node)方法不斷自檢看節點是否已經在CLH同步佇列了,如果是則嘗試獲取鎖,否則一直掛起。當執行緒呼叫signal()方法後,程式首先檢查當前執行緒是否獲取了鎖,然後通過doSignal(Node first)方法喚醒CLH同步佇列的首節點。被喚醒的執行緒,將從await()方法中的while迴圈中退出來,然後呼叫acquireQueued()方法競爭同步狀態。

示例

//生產者-消費者問題
public class ConditionCase {
    private LinkedList<String> buffer;
    private int maxSize;
    private Lock lock;
    private Condition fullCondition;
    private Condition notFullCondition;

    public ConditionCase(int maxSize){
        this.maxSize=maxSize;
        buffer=new LinkedList<>();
        lock=new ReentrantLock();
        fullCondition=lock.newCondition();
        notFullCondition=lock.newCondition();
    }

    public void set(String string) throws InterruptedException {
        lock.lock();
        try {
            while(maxSize==buffer.size()){
                notFullCondition.await();
            }
            buffer.add(string);
            fullCondition.signal();
        } finally {
            lock.unlock();
        }
    }

    public String set() throws InterruptedException {
        String string;
        try {
            lock.lock();
            while(buffer.size()==0){
                fullCondition.await();
            }
            string=buffer.poll();
            notFullCondition.signal();
        } finally {
            lock.unlock();
        }
        return string;
    }
}

複製程式碼

參考資料

相關文章