1. 程式人生 > >J.U.C 之 Condition

J.U.C 之 Condition

在沒有 Lock 之前,我們使用 synchronized 來控制同步,配合 Object 的 #wait()#notify() 等一系列方法可以實現等待 / 通知模式。在 Java SE 5 後,Java 提供了 Lock 介面,相對於 synchronized 而言,Lock 提供了條件 Condition ,對執行緒的等待、喚醒操作更加詳細和靈活。下圖是 Condition 與 Object 的監視器方法的對比(摘自《Java併發程式設計的藝術》):

Condition 與 Object 的監視器方法的對比

1. Condition

java.util.concurrent.locks.Condition ,條件 Condition 介面,定義了一系列的方法,來對阻塞和喚醒執行緒:

// ========== 阻塞 ==========

void await() throws InterruptedException; // 造成當前執行緒在接到訊號或被中斷之前一直處於等待狀態。
void awaitUninterruptibly(); // 造成當前執行緒在接到訊號之前一直處於等待狀態。【注意:該方法對中斷不敏感】。
long awaitNanos(long nanosTimeout) throws InterruptedException; // 造成當前執行緒在接到訊號、被中斷或到達指定等待時間之前一直處於等待狀態。返回值表示剩餘時間,如果在`nanosTimeout` 之前喚醒,那麼返回值 `= nanosTimeout - 消耗時間` ,如果返回值 `<= 0` ,則可以認定它已經超時了。
boolean await(long time, TimeUnit unit) throws InterruptedException; // 造成當前執行緒在接到訊號、被中斷或到達指定等待時間之前一直處於等待狀態。
boolean awaitUntil(Date deadline) throws InterruptedException; // 造成當前執行緒在接到訊號、被中斷或到達指定最後期限之前一直處於等待狀態。如果沒有到指定時間就被通知,則返回 true ,否則表示到了指定時間,返回返回 false 。

// ========== 喚醒 ==========

void signal(); // 喚醒一個等待執行緒。該執行緒從等待方法返回前必須獲得與Condition相關的鎖。
void signalAll(); // 喚醒所有等待執行緒。能夠從等待方法返回的執行緒必須獲得與Condition相關的鎖。

Condition 是一種廣義上的條件佇列。他為執行緒提供了一種更為靈活的等待 / 通知模式,執行緒在呼叫 await 方法後執行掛起操作,直到執行緒等待的某個條件為真時才會被喚醒。Condition 必須要配合 Lock 一起使用,因為對共享狀態變數的訪問發生在多執行緒環境下。一個 Condition 的例項必須與一個 Lock 繫結,因此 Condition 一般都是作為 Lock 的內部實現。

2. ConditionObject

獲取一個 Condition 必須要通過 Lock 的 #newCondition() 方法。該方法定義在介面 Lock 下面,返回的結果是繫結到此 Lock 例項的新 Condition 例項

。Condition 為一個介面,其下僅有一個實現類 ConditionObject ,由於 Condition 的操作需要獲取相關的鎖,而 AQS則是同步鎖的實現基礎,所以 ConditionObject 則定義為 AQS 的內部類。程式碼如下:

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() {
    }

    // ... 省略內部程式碼
}
  • 從上面程式碼可以看出,ConditionObject 擁有首節點(firstWaiter),尾節點(lastWaiter)。當前執行緒呼叫 #await()方法時,將會以當前執行緒構造成一個節點(Node),並將節點加入到該佇列的尾部。結構如下:

    Condition 等待佇列

  • Node 裡面包含了當前執行緒的引用。Node 定義與 AQS 的 CLH 同步佇列的節點使用的都是同一個類(AbstractQueuedSynchronized 的 Node 靜態內部類)。

  • ConditionObject 的佇列結構比 CLH 同步佇列的結構簡單些,新增過程較為簡單,只需要將原尾節點的 Node.next 指向新增節點,然後更新 ConditionObject.lastWaiter 即可。

2.1 大體實現流程

老艿艿:在理解 Condition 的時候,看了下 《Java Condition 原始碼分析》大體實現流程,寫的挺不錯的,所以直接引用。

AQS 等待佇列與 Condition 佇列是兩個相互獨立的佇列

  • #await() 就是在當前執行緒持有鎖的基礎上釋放鎖資源,並新建 Condition 節點加入到 Condition 的佇列尾部,阻塞當前執行緒 。
  • #signal() 就是將 Condition 的頭節點移動到 AQS 等待節點尾部,讓其等待再次獲取鎖。

以下是 AQS 佇列和 Condition 佇列的出入結點的示意圖,可以通過這幾張圖看出執行緒結點在兩個佇列中的出入關係和條件。

I.初始化狀態:AQS等待佇列有 3 個Node,Condition 佇列有 1 個Node(也有可能 1 個都沒有)

img

II.節點1執行 Condition.await()

  1. 將 head 後移
  2. 釋放節點 1 的鎖並從 AQS 等待佇列中移除
  3. 將節點 1 加入到 Condition 的等待佇列中
  4. 更新 lastWaiter 為節點 1

img

III.節點 2 執行 Condition.signal() 操作

  1. 將 firstWaiter後移
  2. 將節點 4 移出 Condition 佇列
  3. 將節點 4 加入到 AQS 的等待佇列中去
  4. 更新 AQS 的等待佇列的 tail

img

2.2 等待

2.2.1 await

呼叫 Condition 的 #await() 方法,會使當前執行緒進入等待狀態,同時會加入到 Condition 等待佇列,並且同時釋放鎖。當從 #await() 方法結束時,當前執行緒一定是獲取了Condition 相關聯的鎖。

public final void await() throws InterruptedException {
    // 當前執行緒中斷
    if (Thread.interrupted())
        throw new InterruptedException();
    //當前執行緒加入等待佇列
    Node node = addConditionWaiter();
    //釋放鎖
    long 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);
}
  • 首先,將當前執行緒新建一個節點同時加入到條件佇列中。
  • 然後,釋放當前執行緒持有的同步狀態。
  • 之後,則是不斷檢測該節點代表的執行緒,出現在 CLH 同步佇列中(收到 signal 訊號之後,就會在 AQS 佇列中檢測到),如果不存在則一直掛起
  • 最後,重新參與競爭,獲取到同步狀態。

2.1.1.1 addConditionWaiter

#addConditionWaiter() 方法,加入條件佇列,程式碼如下:

private Node addConditionWaiter() {
    Node t = lastWaiter;    //尾節點
    //Node的節點狀態如果不為CONDITION,則表示該節點不處於等待狀態,需要清除節點
    if (t != null && t.waitStatus != Node.CONDITION) {
        //清除條件佇列中所有狀態不為Condition的節點
        unlinkCancelledWaiters();
        t = lastWaiter;
    }
    //當前執行緒新建節點,狀態 CONDITION
    Node node = new Node(Thread.currentThread(), Node.CONDITION);
    /**
     * 將該節點加入到條件佇列中最後一個位置
     */
    if (t == null)
        firstWaiter = node;
    else
        t.nextWaiter = node;
    lastWaiter = node;
    return node;
}
  • 該方法主要是將當前執行緒加入到 Condition 條件佇列中。當然,在加入到尾節點之前,會呼叫 #unlinkCancelledWaiters() 方法,清除所有狀態不為 Condition 的節點。

2.1.1.2 fullyRelease

#fullyRelease(Node node) 方法,負責完全釋放該執行緒持有的鎖,因為例如 ReentrantLock 是可以重入的。程式碼如下:

final long fullyRelease(Node node) {
    boolean failed = true;
    try {
        // 節點狀態--其實就是持有鎖的數量
        long savedState = getState();
        // 釋放鎖
        if (release(savedState)) {
            failed = false;
            return savedState;
        } else {
            throw new IllegalMonitorStateException();
        }
    } finally {
        if (failed)
            node.waitStatus = Node.CANCELLED;
    }
}
  • 正常情況下,釋放鎖都能成功,因為是呼叫 Lock#lock() 方法,呼叫 Condition#await() 方法。
  • 那麼什麼情況下會失敗,丟擲 IllegalMonitorStateException 異常呢?例如,當前執行緒未持有鎖,呼叫 Lock#lock() 方法,而直接呼叫 Condition#await() 方法,此時就會丟擲該異常。
  • 另外,釋放失敗的情況下,會設定 Node 的等待狀態為 Node.CANCELED

2.1.1.3 isOnSyncQueue

#isOnSyncQueue(Node node) 方法,如果一個節點剛開始在條件佇列上,現在在同步佇列上獲取鎖則返回 true 。程式碼如下:

final boolean isOnSyncQueue(Node node) {
    // 狀態為 Condition,獲取前驅節點為 null ,返回 false
    if (node.waitStatus == Node.CONDITION || node.prev == null)
        return false;
    // 後繼節點不為 null,肯定在 CLH 同步佇列中
    if (node.next != null)
        return true;

    return findNodeFromTail(node);
}

2.1.1.4 unlinkCancelledWaiters

#unlinkCancelledWaiters() 方法,負責將條件佇列中狀態不為 Condition 的節點刪除。程式碼如下:

// 等待佇列是一個單向連結串列,遍歷連結串列將已經取消等待的節點清除出去
// 純屬連結串列操作,很好理解,看不懂多看幾遍就可以了
private void unlinkCancelledWaiters() {
    Node t = firstWaiter;
    Node trail = null; // 用於中間不需要跳過時,記錄上一個 Node 節點
    while (t != null) {
        Node next = t.nextWaiter;
        // 如果節點的狀態不是 Node.CONDITION 的話,這個節點就是被取消的
        if (t.waitStatus != Node.CONDITION) {
            t.nextWaiter = null;
            if (trail == null)
                firstWaiter = next;
            else
                trail.nextWaiter = next;
            if (next == null)
                lastWaiter = trail;
        }
        else
            trail = t;
        t = next;
    }
}

2.2.2 其他 await 實現方法

2.3 通知

2.3.1 signal

呼叫 ConditionObject的 #signal() 方法,將會喚醒在等待佇列中等待最長時間的節點(條件佇列裡的首節點),在喚醒節點前,會將節點移到CLH同步佇列中。

public final void signal() {
    //檢測當前執行緒是否為擁有鎖的獨
    if (!isHeldExclusively())
        throw new IllegalMonitorStateException();
    //頭節點,喚醒條件佇列中的第一個節點
    Node first = firstWaiter;
    if (first != null)
        doSignal(first);    //喚醒
}
  • 該方法首先會判斷當前執行緒是否已經獲得了鎖,這是前置條件。然後呼叫 #doSignal(Node first) 方法,喚醒條件佇列中的頭節點。程式碼如下:

    private void doSignal(Node first) {
        do {
            //修改頭結點,完成舊頭結點的移出工作
            if ( (firstWaiter = first.nextWaiter) == null)
                lastWaiter = null;
            first.nextWaiter = null;
        } while (!transferForSignal(first) &&
                (first = firstWaiter) != null);
    }
    
    • 主要是做兩件事:1)修改頭節點;2)呼叫 #transferForSignal(Node first) 方法將節點移動到 CLH 同步佇列中。程式碼如下:

       final boolean transferForSignal(Node node) {
          //將該節點從狀態CONDITION改變為初始狀態0,
          if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
              return false;
      
          //將節點加入到syn佇列中去,返回的是syn佇列中node節點前面的一個節點
          Node p = enq(node);
          int ws = p.waitStatus;
          //如果結點p的狀態為cancel 或者修改waitStatus失敗,則直接喚醒
          if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
              LockSupport.unpark(node.thread);
          return true;
      }
      
      • x

整個通知的流程如下:

  1. 判斷當前執行緒是否已經獲取了鎖,如果沒有獲取則直接丟擲異常,因為獲取鎖為通知的前置條件。
  2. 如果執行緒已經獲取了鎖,則將喚醒條件佇列的首節點
  3. 喚醒首節點是先將條件佇列中的頭節點移出,然後呼叫 AQS 的 #enq(Node node) 方法將其安全地移到 CLH 同步佇列中
  4. 最後判斷如果該節點的同步狀態是否為 Node.CANCEL ,或者修改狀態為 Node.SIGNAL 失敗時,則直接呼叫 LockSupport 喚醒該節點的執行緒。

2.3.2 signalAll

老艿艿:感興趣的胖友,自己去檢視。

2.4 總結

一個執行緒獲取鎖後,通過呼叫 Condition 的 #await() 方法,會將當前執行緒先加入到條件佇列中,然後釋放鎖,最後通過 #isOnSyncQueue(Node node) 方法,不斷自檢看節點是否已經在 CLH 同步隊列了,如果是則嘗試獲取鎖,否則一直掛起。

當執行緒呼叫 #signal() 方法後,程式首先檢查當前執行緒是否獲取了鎖,然後通過#doSignal(Node first) 方法喚醒CLH同步佇列的首節點。被喚醒的執行緒,將從 #await() 方法中的 while 迴圈中退出來,然後呼叫 #acquireQueued(Node node, int arg) 方法競爭同步狀態。

3. Condition 的應用

只知道原理,如果不知道使用那就坑爹了,下面是用Condition實現的生產者消費者問題:

public class ConditionTest {
    private LinkedList<String> buffer;    //容器
    private int maxSize ;           //容器最大
    private Lock lock;
    private Condition fullCondition;
    private Condition notFullCondition;

    ConditionTest(int maxSize){
        this.maxSize = maxSize;
        buffer = new LinkedList<String>();
        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 get() throws InterruptedException {
        String string;
        lock.lock();
        try {
            while (buffer.size() == 0){
                fullCondition.await();
            }
            string = buffer.poll();
            notFullCondition.signal();
        } finally {
            lock.unlock();
        }
        return string;
    }
}

參考資料