併發編程學習(5)Condition

Condition

在前面學習 synchronized 的時候,有wait/notify 的基本使用,結合 synchronized 可以實現對線程的通信。那麼,既然 J.U.C 裏面提供了鎖的實現機制,那 J.U.C 裏面應該也有提供線程通信的機制,Condition 是一個多線程協調通信的工具類,可以讓某些線程一起等待某個條件(condition),只有滿足條件時,線程纔會被喚醒。

測試代碼

Signal

public class Signal implements Runnable{
    private Lock lock;
    private Condition condition;

    public Signal(Lock lock, Condition condition) {
        this.lock = lock;
        this.condition = condition;
    }

    @Override
    public void run() {
        lock.lock();
        try {
            System.out.println("Signal--開始");
            condition.signal();
            System.out.println("Signal--結束");
        }finally {
            lock.unlock();
        }
    }
}

Wait

public class Wait implements Runnable{
    private Lock lock;
    private Condition condition;

    public Wait(Lock lock, Condition condition) {
        this.lock = lock;
        this.condition = condition;
    }

    @Override
    public void run() {
        lock.lock();
        try {
            try {
                System.out.println("wait--開始");
                condition.await();
                System.out.println("wait--結束");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }finally {
            lock.unlock();
        }
    }
}

測試

public static void main(String[] args) {
    Lock lock = new ReentrantLock();
    Condition condition = lock.newCondition();
    new Thread(new Wait(lock, condition)).start();
    new Thread(new Conditions(lock, condition)).start();
}
------------------------輸出
wait--開始
Signal--開始
Signal--結束
wait--結束

condition分析

調用 Condition,需要獲得 Lock 鎖,所以意味着會存在一個 AQS 同步隊列。先進入await方法分析

public final void await() throws InterruptedException {
        if (Thread.interrupted())     //這也就是表示await的線程允許被中斷 這是lock的一大特性
            throw new InterruptedException(); //如果當前線程被中斷,則拋出InterruptedException 
        Node node = addConditionWaiter();  //創建一個新的節點,節點狀態爲 condition,採用的數據結構仍然是鏈表
        int savedState = fullyRelease(node); //釋放當前的鎖,得到鎖的狀態,並喚醒 AQS 隊列中的一個線程
        int interruptMode = 0; //如果當前節點沒有在同步隊列上,即還沒有被 signal,則將當前線程阻塞
        while (!isOnSyncQueue(node)) { //判斷這個節點是否在 AQS 隊列上,第一次判斷的是 false,因爲前面已經釋放鎖了
            LockSupport.park(this); //通過 park 掛起當前線程
            if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
                break;
        }
        // 當這個線程醒來,會嘗試拿鎖, 當 acquireQueued返回 false 就是拿到鎖了.
       // interruptMode != THROW_IE -> 表示這個線程沒有成功將 node 入隊,但 signal 執行了 enq 方法讓其入隊了.
        if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
            // 將這個變量設置成 REINTERRUPT.
            interruptMode = REINTERRUPT;
       // 如果 node 的下一個等待者不是 null, 則進行清理,清理 Condition 隊列上的節點 如果是 null ,就沒有什麼好清理了.
        if (node.nextWaiter != null) // clean up if cancelled
            unlinkCancelledWaiters();
        // 如果線程被中斷了,需要拋出異常.或者什麼都不做
        if (interruptMode != 0)
            reportInterruptAfterWait(interruptMode);
    }

addConditionWaiter
這個方法的主要作用是把當前線程封裝成 Node,添加到等待隊列。這裏的隊列不再是雙向鏈表,而是單向鏈表

private Node addConditionWaiter() {
//如 果 lastWaiter 不 等 於 空 並 且waitStatus 不等於 CONDITION 時,把衝好這個節點從鏈表中移除
        Node t = lastWaiter;
        // If lastWaiter is cancelled, clean out.
        if (t != null && t.waitStatus != Node.CONDITION) {
            unlinkCancelledWaiters();
            t = lastWaiter;
        }
   //構建一個 Node,waitStatus=CONDITION。這裏的鏈表是一個單向的,所以相比 AQS 來說會簡單很多
        Node node = new Node(Thread.currentThread(), Node.CONDITION);
        if (t == null)
            firstWaiter = node;
        else
            t.nextWaiter = node;
        lastWaiter = node;
        return node;
}

fullyRelease
徹底的釋放鎖,什麼叫徹底呢,就是如果當前鎖存在多次重入,那麼在這個方法中只需要釋放一次就會把所有的重入次數歸零。

final int fullyRelease(Node node) {
    boolean failed = true;
        try {
             // 獲得重入的次數
            int savedState = getState();
            // 釋放鎖並且喚醒下一個同步隊列中的線程
            if (release(savedState)) {
                failed = false;
                return savedState;
            } else {
                throw new IllegalMonitorStateException();
            }
        } finally {
            if (failed)
                node.waitStatus = Node.CANCELLED;
        }
    }

isOnSyncQueue
判斷當前節點是否在同步隊列中,返回 false 表示不在,返回 true 表示在
如果不在 AQS 同步隊列,說明當前節點沒有喚醒去爭搶同步鎖,所以需要把當前線程阻塞起來,直到其他的線程調用 signal 喚醒
如果在 AQS 同步隊列,意味着它需要去競爭同步鎖去獲得執行程序執行權限爲什麼要做這個判斷呢?原因是在 condition 隊列中的節
點會重新加入到 AQS 隊列去競爭鎖。也就是當調用 signal的時候,會把當前節點從 condition 隊列轉移到 AQS 隊列

  1. 如果 ThreadA 的 waitStatus 的狀態爲 CONDITION,說明它存在於 condition 隊列中,不在 AQS 隊列。因爲AQS 隊列的狀態一定不可能有 CONDITION
  2. 如果 node.prev 爲空,說明也不存在於 AQS 隊列,原因是 prev=null 在 AQS 隊列中只有一種可能性,就是它是head 節點,head 節點意味着它是獲得鎖的節點。
  3. 如果 node.next 不等於空,說明一定存在於 AQS 隊列中,因爲只有 AQS 隊列纔會存在 next 和 prev 的關係
  4. findNodeFromTail,表示從 tail 節點往前掃描 AQS 隊列,一旦發現 AQS 隊列的節點和當前節點相等,說明節點一定存在於 AQS 隊列中
   final boolean isOnSyncQueue(Node node) {
        if (node.waitStatus == Node.CONDITION || node.prev == null)
            return false;
        if (node.next != null) // If has successor, it must be on queue
            return true;
        /*
         * node.prev可以是非空的,但尚未在隊列中,
         * 因爲將CAS放在隊列中的CAS可能會失敗。因此,我們必須從尾部進行遍歷,
         * 以確保它實際成功。在調用這種方法時,總是接近尾部,除非CAS失敗(這不太可能),
         * 它將在那裏,所以我們幾乎不會遍歷很多
         */
        return findNodeFromTail(node);
    }

Condition.signal
await 方法會阻塞 wait,然後 Signal 搶佔到了鎖獲得了執行權限,這個時候在 Signal 中調用了 Condition的 signal()方法,將會喚醒在等待隊列中節點

 public final void signal() {
  //先判斷當前線程是否獲得了鎖,這個判斷比較簡單,直接用獲得鎖的線程和當前線程相比即可 
  所以你如果沒獲得鎖來調用這個方法是不對的,會拋這個異常
        if (!isHeldExclusively())
            throw new IllegalMonitorStateException();
  //拿到 Condition隊列上第一個節點
        Node first = firstWaiter;
        if (first != null)
            doSignal(first);
  }

doSignal
對 condition 隊列中從首部開始的第一個 condition 狀態的節點,執行 transferForSignal 操作,將 node 從 condition隊列中轉換到 AQS 隊列中,同時修改 AQS 隊列中原先尾節點的狀態

    private void doSignal(Node first) {
        do {
        // 從 Condition 隊列中刪除 first 節點
            if ( (firstWaiter = first.nextWaiter) == null)
                lastWaiter = null;
           // 將 next 節點設置成 null
            first.nextWaiter = null;
        } while (!transferForSignal(first) &&
                 (first = firstWaiter) != null);
 }

AQS.transferForSignal

  final boolean transferForSignal(Node node) {
  //更新節點的狀態爲 0,如果更新失敗,只有一種可能就是節點被 CANCELLED(取消) 了
        if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
            return false;
  //調用 enq--這是aqs的方法了,把當前節點添加到AQS 隊列。並且返回返回按當前節點的上一個節點,也就是原tail 節點
        Node p = enq(node);
        int ws = p.waitStatus;
 // 如果上一個節點的狀態被取消了, 或者嘗試設置上一個節點的狀態爲 SIGNAL 失敗了(SIGNAL 表示: 他的 next節點需要停止阻塞)
        if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
        // 喚醒節點上的線程
            LockSupport.unpark(node.thread);
        return true; //如果 node 的 prev 節點已經是signal 狀態,那麼被阻塞的 wait 的喚醒工作由 AQS 隊列來完成 
  }

這裏執行完畢就變成了執行finally塊裏面的 lock.unlock();來釋放鎖
這個時候會判斷 wait 的 prev 節點也就是 head 節點的 waitStatus,如果大於 0 或者設置 SIGNAL 失敗,表示節點被設置成了 CANCELLED 狀態。這個時候會喚醒wait 這個線程。否則就基於 AQS 隊列的機制來喚醒,也就是等到 Signal 釋放鎖之後來喚醒 wait線程

被阻塞的線程喚醒後的邏輯
前面在 await 方法時,線程會被阻塞。而通過 signal被喚醒之後又繼續回到上次執行箭頭指向的checkInterruptWhileWaiting 這個方法,其實從名字就可以看出來判斷線程在等待狀態下是否收到中斷的請求,就是 Wait線程 在 condition 隊列被阻塞的過程中,有沒有被其他線程觸發過中斷請求

這裏需要注意的地方是,如果第一次 CAS 失敗了,則不能判斷當前線程是先進行了中斷還是先進行了 signal 方法的調用,可能是先執行了 signal 然後中斷,也可能是先執行了中斷,後執行了 signal,當然,這兩個操作肯定是發生在 CAS 之前。這時需要做的就是等待當前線程的 node被添加到 AQS 隊列後,也就是 enq 方法返回後,返回false 告訴 checkInterruptWhileWaiting 方法返回REINTERRUPT(1),後續進行重新中斷。簡單來說,該方法的返回值代表當前線程是否在 park 的時候被中斷喚醒,如果爲 true 表示中斷在 signal 調用之前,signal 還未執行,那麼這個時候會根據await的語義,在 await時遇到中斷需要拋出interruptedException,返回 true 就是告訴checkInterruptWhileWaiting 返回 THROW_IE(-1)。如果返回 false,否則表示signal 已經執行過了,只需要重新響應中斷即可。這裏的主要目的就是判斷線程在等待的時候如果有中斷請求,就一定要把它中斷了

  private int checkInterruptWhileWaiting(Node node) {
            return Thread.interrupted() ?
                (transferAfterCancelledWait(node) ? THROW_IE : REINTERRUPT) :
                0;
        }
    final boolean transferAfterCancelledWait(Node node) {
    //使用 cas 修改節點狀態,如果還能修改成功,說明線程被中斷時,signal 還沒有被調用。
    // 這裏有一個知識點,就是線程被喚醒,並不一定是在 java 層面執行了locksupport.unpark,
    也可能是調用了線程的 interrupt()方法,這個方法會更新一箇中斷標識,並且會喚醒處於阻塞狀態下的線程。
        if (compareAndSetWaitStatus(node, Node.CONDITION, 0)) {
            enq(node); //如果 cas 成功,則把node 添加到 AQS 隊列
            return true;
        }
    // 如果 cas 失敗,則判斷當前 node 是否已經在 AQS 隊列上,如果不在,
    則讓給其他線程執行當 node 被觸發了 signal 方法時,node 就會被加到 aqs 隊列上
        while (!isOnSyncQueue(node))
        //循環檢測 node 是否已經成功添加到 AQS 隊列中。如果沒有,則通過 yield,
            Thread.yield();
        return false;
    }

acquireQueued
通過AQS隊列修改等待隊列的線程狀態
當前被喚醒的節點wait線程 去搶佔同步鎖。並且要恢復到原本的重入次數狀態。調用完這個方法之後,AQS 隊列的狀態如下
將 head 節點的 waitStatus 設置爲-1,Signal 狀態。

reportInterruptAfterWait
根據 checkInterruptWhileWaiting 方法返回的中斷標識來進行中斷上報。如果是 THROW_IE,則拋出中斷異常如果是 REINTERRUPT,則重新響應中斷

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