併發類Condition原始碼分析

我們一直在路上發表於2020-12-06

一、用ReentrantLock加condition完成生產消費者模型

在前面學習 synchronized 的時候,有講到 wait/notify 的基本使用,結合 synchronized 可以實現對執行緒的通訊。那麼這個時候我就在思考了,既然 J.U.C 裡面提供了鎖的實現機制,那 J.U.C 裡面有沒有提供類似的執行緒通訊的工具呢? 於是找阿找,發現了一個Condition 工具類。Condition 是一個多執行緒協調通訊的工具類,可以讓某些執行緒一起等待某個條件(condition),只有滿足條件時,執行緒才會被喚醒。
condition基本使用:
生產者:

package com.gupao.condition;

import java.util.Queue;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;

public class Product implements Runnable {

    private Queue<Integer> queue;

    private Lock lock;

    private Condition condition;

    private int maxSize;

    public Product(Queue<Integer> queue, Lock lock, Condition condition, int maxSize) {
        this.queue = queue;
        this.lock = lock;
        this.condition = condition;
        this.maxSize = maxSize;
    }

    @Override
    public void run() {

        int i = 0;
        while (true) {
            i++;
            lock.lock();
            while (queue.size() == maxSize) {
                try {
                    System.out.println("佇列滿了!");
                    condition.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            queue.add(i);
            System.out.println("生產者生產訊息" + i);
            condition.signal();
            lock.unlock();
        }

    }
}

消費者:

package com.gupao.condition;

import java.util.Queue;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;

public class Customer implements Runnable {

    // 對列
    private Queue<Integer> queue;

    // 鎖
    private Lock lock;

    private Condition condition;

    // 佇列的大小
    private int maxSize;

    public Customer(Queue<Integer> queue, Lock lock, Condition condition, int maxSize) {
        this.queue = queue;
        this.lock = lock;
        this.condition = condition;
        this.maxSize = maxSize;
    }

    @Override
    public void run() {

        int i = 0;
        while (true) {
            i++;
            lock.lock();
            while (queue.size() == 0) {
                try {
                    System.out.println("佇列空了!");
                    condition.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            Integer remove = queue.remove();
            System.out.println("消費者消費訊息" + remove);
            condition.signal();
            lock.unlock();
        }
    }
}

測試類:

	public class main {

    public static void main(String[] args) throws InterruptedException {
        Queue<Integer> queue = new LinkedBlockingQueue<>();
        int maxSize = 5;
        Lock lock = new ReentrantLock();
        Condition condition = lock.newCondition();
        Thread t1 = new Thread(new Product(queue,lock,condition,maxSize));
        Thread t2 = new Thread(new Customer(queue, lock, condition, maxSize));

        t2.start();
        Thread.sleep(500);
        t1.start();
    }
}

二、原始碼分析

該方法是建立在已經拿到鎖的情況下。想了解lock的原理的可以看我另外一篇部落格
ReentrantLock原理分析
呼叫Condition,需要獲得 Lock 鎖,所以意味著會存在一個 AQS 同步佇列,在上面那個案例中,假如兩個執行緒同時執行的話,那麼 AQS 的佇列可能是下面這種情況
在這裡插入圖片描述
呼叫Condition 的await()方法(或者以 await 開頭的方法),會使當前執行緒進入等待佇列並釋放鎖,同時執行緒狀態變為等待狀態。

public final void await() throws InterruptedException {
			// 該方法允許被中斷
            if (Thread.interrupted())
                throw new InterruptedException(); 
            // 建立一個新的節點加入condition佇列中,節點狀態為condition
            Node node = addConditionWaiter();
            // 釋放當前的鎖,得到鎖的狀態,並喚醒 AQS 佇列中的一個執行緒
            int savedState = fullyRelease(node);
            int interruptMode = 0;
            // 如果當前節點沒有在同步佇列上,即還沒有被 signal,則將當前執行緒阻塞
            while (!isOnSyncQueue(node)) {
            // 判斷這個節點是否在 AQS 佇列上,第一次判斷的是 false,因為前面已經釋放鎖了
            // 通過 park 掛起當前執行緒
                LockSupport.park(this);
                if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
                    break;
            }
            // 當這個執行緒醒來,會嘗試拿鎖, 當acquireQueued返回 false 就是拿到鎖了.
			// interruptMode != THROW_IE -> 表示這個執行緒沒有成功將 node 入隊,但 signal 執行了 enq 方法讓其入隊了.
			// 將這個變數設定成 REINTERRUPT.
            if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
                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() {
            Node t = lastWaiter;
            // If lastWaiter is cancelled, clean out.
            // 如果最後一個節點不等於condtion狀態,則從連結串列中刪除,不是condition狀態就是cancelled狀態
            if (t != null && t.waitStatus != Node.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;
        }

unlinkCancelledWaiters
這個方法是清理掉連結串列中的狀態為cancelled狀態的節點

 private void unlinkCancelledWaiters() {
            Node t = firstWaiter;
            Node trail = null;
            // 如果首節點不為空
            while (t != null) {
            	// 獲取到下個節點
                Node next = t.nextWaiter;
                // 如果該節點的狀態不等於conditon,則該節點需要在連結串列中刪除
                if (t.waitStatus != Node.CONDITION) {
                	// 該節點的下個節點設定為空,意味著垃圾回收後就回收該節點
                    t.nextWaiter = null;
                    // trail 為空,則把下一個節點負責給首節點
                    if (trail == null)
                        firstWaiter = next;
                    else
                    	// 把下一個節點賦值給next,這樣連結串列就要繼續連線起來
                        trail.nextWaiter = next;
                    if (next == null)
                        lastWaiter = trail;
                }
                // 等於condtion,把該節點賦值給尾節點
                else
                    trail = t;
                // 下個一個節點賦值給t,進行下一次迴圈
                t = next;
            }
        }

圖解分析
執行完addConditionWaiter 這個方法之後,就會產生一個這樣的condition 佇列
在這裡插入圖片描述
fullyRelease
fullRelease,就是徹底的釋放鎖,什麼叫徹底呢,就是如果當前鎖存在多次重入,那麼在這個方法中只需要釋放一次就會把所有的重入次數歸零。

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

圖解分析

此時,同步佇列會觸發鎖的釋放和重新競爭。ThreadB 獲得了鎖。
在這裡插入圖片描述
isOnSyncQueue
判斷當前節點是否在同步佇列中,返回 false 表示不在,返回true 表示如果不在,
講當前執行緒掛起

final boolean isOnSyncQueue(Node node) {
		// 如果狀態是condition,證明一定不再同步佇列裡,condition狀態只存在於等待佇列,在同步佇列裡,node.prev是一定不為空的,因為有個head的節點
        if (node.waitStatus == Node.CONDITION || node.prev == null)
            return false;
        // 在等待佇列裡,node.next 是等於空的,不等於空就是在同步佇列當中
        if (node.next != null) // If has successor, it must be on queue
            return true;
       // 遍歷正個同步佇列,判斷node是否在同步佇列當中
        return findNodeFromTail(node);
    }

await 方法會阻塞 ThreadA,然後 ThreadB 搶佔到了鎖獲得了執行許可權,這個時候在 ThreadB 中呼叫了 Condition 的 signal()方法,將會喚醒在等待佇列中節點

public final void signal() {
	// 判斷當前執行緒是否獲取到了鎖,如果沒有拋異常
            if (!isHeldExclusively())
                throw new IllegalMonitorStateException();
            Node first = firstWaiter;
            // 如果首節點不為空,喚醒首節點
            if (first != null)
                doSignal(first);
        }

doSignal

private void doSignal(Node first) {
            do {
            // frist的下一個節點如果為空,就把lastWaiter設定為空
                if ( (firstWaiter = first.nextWaiter) == null)
                    lastWaiter = null;
             // 不為空,再把first 節點從等待佇列中移除
                first.nextWaiter = null;
            } while (!transferForSignal(first) &&
            	// 返回false,firstWaiter已經被從新賦值過了,如果不是空,進行下一次遍歷
                     (first = firstWaiter) != null);
        }

transferForSignal

final boolean transferForSignal(Node node) {
       // 如果該節點不是condition狀態(可能程式設計了cancelled狀態),waitStatus=0,就會設定失敗,返回false
        if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
            return false;
	// 將該節點放入到AQS佇列中,返回上一個節點
        Node p = enq(node);
        int ws = p.waitStatus;
        // 如果上一個節點狀態沒有被改變,也就是沒有程式設計cancelled狀態,就將該節點狀態設定成singal狀態
        if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
        // 如果狀態已經是cancelled狀態,將該節點的執行緒掛起
            LockSupport.unpark(node.thread);
        return true;
    }

被阻塞的執行緒喚醒後的邏輯

前面在分析await 方法時,執行緒會被阻塞。而通過 signal被喚醒之後又繼續回到上次執行的邏輯中

		while (!isOnSyncQueue(node)) {
           LockSupport.park(this);
            if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
                    break;
        }

checkInterruptWhileWaiting

 private int checkInterruptWhileWaiting(Node node) {
            return Thread.interrupted() ?
                (transferAfterCancelledWait(node) ? THROW_IE : REINTERRUPT) :
                0;
        }

transferAfterCancelledWait
進入到該方法,證明該執行緒被中斷過。
這裡有個知識點,就是進入該執行緒進入該方法,不一定是被signal喚醒點,也有可能是是呼叫了該執行緒的interrupt()方法,這個方法會更新一箇中斷標識,並且會喚醒處於阻塞狀態下的執行緒。

final boolean transferAfterCancelledWait(Node node) {
		// 如果cas成功,代表是被執行緒中斷的,放入到AQS佇列,返回true
        if (compareAndSetWaitStatus(node, Node.CONDITION, 0)) {
            enq(node);
            return true;
        }
        // 如果cas 失敗,返回false
        while (!isOnSyncQueue(node))
            Thread.yield();
        return false;
    }

如果返回true,證明這個沒有執行被signal喚醒,就被中斷了,checkInterruptWhileWaiting返回THROW_IE
如果返回false, 證明這個這個方法是被喚醒以後,在被中斷的
checkInterruptWhileWaiting返回REINTERRUPT

reportInterruptAfterWait
如果是THROW_IE,則丟擲中斷異常
如果是REINTERRUPT,則重新響應中斷

 private void reportInterruptAfterWait(int interruptMode)
            throws InterruptedException {
            // 如果是被中斷喚醒的,執行緒直接拋異常
            if (interruptMode == THROW_IE)
                throw new InterruptedException();
            // 如果是被
            else if (interruptMode == REINTERRUPT)
                selfInterrupt();
        }

相關文章