併發學習(六)Condition的使用及原理

wait和notify實現生產者消費者模式

講Condition之前,有必要再熟悉下wait和notify結合synchronized實現線程的通信,比如實現生產者和消費者模式。案例代碼如下。

public class ProductConsumer {
    private int queueSize = 10;
    private PriorityQueue<Integer> queue = new PriorityQueue<Integer>(queueSize);

    public static void main(String[] args) throws InterruptedException {
        ProductConsumer test = new ProductConsumer();
        Thread producer = test.new Producter();
        Thread consumer = test.new Consumer();
        producer.start();
        consumer.start();
    }

    //生產者線程
    class Producter extends Thread {

        @Override
        public void run() {
            while (true) {
                //同步代碼塊,獲取隊列鎖
                synchronized (queue) {
                    //當隊列不滿時生產者可以繼續生產,生產之後喚醒消費者
                    //喚醒消費者,在生產者釋放鎖之後,消費者不一定就會獲取鎖,也許是生產者獲取到鎖繼續執行
                    //但是如果不喚醒生產者,當隊列滿時,如果消費者處於阻塞狀態,那麼生產者和消費者都處於阻塞狀態,程序就無法繼續執行
                    if (queue.size() < queueSize) {
                        queue.add(queue.size() + 1);
                        System.out.println("生產者向隊列中加入產品P,隊列剩餘空間:" + (queueSize - queue.size()));
                        try {
                            //模擬生產者生產過程,sleep不會釋放鎖
                            Thread.sleep(1000);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        //喚醒消費者
                        queue.notify();//1)隨機生產和消費
                    } else {
                        try {
                            System.out.println("隊列已滿等待消費者消費");
                            //隊列已滿,進入阻塞狀態,等待消費者消費
                            queue.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                            queue.notify();
                        }
                    }
                }
            }

        }
    }

    //消費者線程
    class Consumer extends Thread {
        @Override
        public void run() {
            while (true) {
                //同步代碼塊,獲取隊列鎖
                synchronized (queue) {
                    //如果隊列是空的,消費者進入阻塞狀態,等待生產者生產並喚醒
                    if (queue.isEmpty()) {
                        System.out.println("沒有產品可以消費,進入阻塞狀態等待生產者生產。");
                        try {
                            //進入阻塞狀態釋放隊列鎖,因爲只有兩個線程,所以生產者一定會獲取到隊列鎖執行
                            queue.wait();//1)隨機生產和消費
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                            //如果發送異常,主動喚醒生產者線程執行
                            queue.notify();
                        }
                        //System.out.println("消費者獲取到隊列鎖準備消費");
                    } else {
                        //如果隊列不空,就消費產品,並喚醒生產者
                        //注意喚醒生產者,在消費者執行完畢釋放鎖之後,不一定生產者就會獲得鎖,也許消費者會繼續獲取鎖執行
                        //但是如果不喚醒生產者,那麼如果生產者處於阻塞狀態,當隊列爲空,消費者也進入阻塞狀態那麼就沒有線程可以獲取鎖繼續執行了
                        queue.notify();//1)隨機生產和消費
                        try {
                            //模擬消費者消費過程,sleep不會釋放鎖
                            Thread.sleep(1000);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        queue.poll();
                        System.out.println("消費者消費了產品P,剩餘空間:" + (queueSize - queue.size()));
                    }
                }
            }
        }
    }
}

執行結果:

生產者向隊列中加入產品P,隊列剩餘空間:9
消費者消費了產品P,剩餘空間:10
沒有產品可以消費,進入阻塞狀態等待生產者生產。
生產者向隊列中加入產品P,隊列剩餘空間:9
消費者消費了產品P,剩餘空間:10
生產者向隊列中加入產品P,隊列剩餘空間:9

通俗的理解就是,廚師不停的炒菜並將炒好的菜放入廚櫃,服務員不停的從廚櫃裏端菜。過程1:假設,服務員把菜端完了發現廚櫃裏已經空了,他會去等待(this.wait),直到廚師炒好菜放入廚櫃並通知他。
過程2:服務員在等待的過程中,廚師繼續不停的炒菜,然後廚櫃被放滿了,這時候就通知服務員可以端菜了(this.notify)。
過程3:過程1和過程2是兩種極端的情況,其實生活中很少出現廚櫃菜都放滿了還不見服務員端菜的。常見的情況都是服務員時不時看下是不是有炒好的菜了,有的話就端出去。java線程也是一樣,生產者線程和消費者線程執行都是隨機的,誰先獲得鎖誰先執行,另一個就阻塞。

Condition用法

還記得之前說的synchronized和lock嗎?這兩者的關係就好比【wait和notify】和condition。只不過condition和lock一樣都是基於java來實現,我們可以深入到源碼來一探究竟。
ConditionWait.java

public class ConditionWait implements Runnable {

    private Lock lock;
    private Condition condition;

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

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

ConditionNotify.java

public class ConditionNotify implements Runnable {

    private Lock lock;
    private Condition condition;

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

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

ConditionTest.java

public class ConditionTest {

    public static void main(String[] args) {

        Lock lock = new ReentrantLock();
        Condition condition = lock.newCondition();
        ConditionWait conditionWait = new ConditionWait(lock, condition);
        ConditionNotify conditionNotify = new ConditionNotify(lock, condition);
        new Thread(conditionWait).start();
        new Thread(conditionNotify).start();
    }
}

執行結果:

ConditionWait開始阻塞...
ConditionNotify開始喚醒...
ConditionNotify結束喚醒...
ConditionWait結束阻塞...

Condition原理分析

上面的condition代碼用法跟wait和notify用法差不多,現在就來分析它的原理。分析原理還是跟之前的AQS一樣,使用情節來描述整個執行過程。

情節1: 上面的兩個類對應的線程這裏直接稱呼wait線程和notify線程,一開始這兩個線程都是執行lock.lock();兩個線程都去爭搶鎖,假設wait線程優先獲得鎖,那麼應該是這個樣子。
在這裏插入圖片描述
情節2: 既然notify線程爭搶鎖失敗,那麼就會封裝成Node加入到同步隊列裏。
【同步隊列】
在這裏插入圖片描述
情節3: wait線程獲得鎖後執行代碼condition.await();這裏wait線程將會阻塞。阻塞之後將會怎麼辦呢?

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

private Node addConditionWaiter() {
    Node t = lastWaiter;
    // If lastWaiter is cancelled, clean out.
    if (t != null && t.waitStatus != Node.CONDITION) {
        unlinkCancelledWaiters();
        t = lastWaiter;
    }
    Node node = new Node(Thread.currentThread(), Node.CONDITION);
    if (t == null)
        firstWaiter = node;
    else
        t.nextWaiter = node;
    lastWaiter = node;
    return node;
}

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

源碼裏跟AQS很相似,這裏會將wait線程封裝成Node節點,並且頭結點和尾結點都指向它,只不過這裏的頭結點和尾結點使用firstWaiter和lastWaiter來表示了。假設還有線程C也執行了condition.await();阻塞,那麼就會追加在wait線程的後面,過程大概是這個樣子。
【等待隊列】
在這裏插入圖片描述
這個隊列跟AQS隊列的不同在於,這個是單向鏈表,稱之爲等待隊列,AQS稱之爲同步隊列。

情節4: 我們知道wait線程阻塞了就需要釋放鎖,而前面爭搶鎖失敗的notify線程就會從同步隊列裏被喚醒。當執行到代碼:condition.signal();時,意味着wait線程又會被喚醒了,但是喚醒是在同步隊列裏進行的,所以這裏會先將處於等待隊列的wait線程轉移到同步隊列裏並喚醒。

public final void signal() {
    if (!isHeldExclusively())
        throw new IllegalMonitorStateException();
    Node first = firstWaiter;
    if (first != null)
        doSignal(first);
}

private void doSignal(Node first) {
    do {
        if ( (firstWaiter = first.nextWaiter) == null)
            lastWaiter = null;
        first.nextWaiter = null;
    } while (!transferForSignal(first) &&
             (first = firstWaiter) != null);
}
// 當前節點從等待隊列轉移至同步隊列
final boolean transferForSignal(Node node) {
    /*
     * If cannot change waitStatus, the node has been cancelled.
     */
    if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
        return false;

    /*
     * Splice onto queue and try to set waitStatus of predecessor to
     * indicate that thread is (probably) waiting. If cancelled or
     * attempt to set waitStatus fails, wake up to resync (in which
     * case the waitStatus can be transiently and harmlessly wrong).
     */
    Node p = enq(node);
    int ws = p.waitStatus;
    if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
        LockSupport.unpark(node.thread);
    return true;
}

在這裏插入圖片描述

總結

關於condition的原理會有兩種隊列出現,一種是AQS同步隊列,另一種是condition等待隊列。首先是在多個線程爭搶鎖的時候,對於一些獲得鎖失敗的線程需要加入到同步隊列裏,而對於主動調用await方法阻塞的線程,則會放入到等待隊列裏。當線程調用signal方法時,處於等待隊列裏的線程纔會被喚醒,但是由於喚醒線程只能在AQS同步隊列裏進行,所以還需要先將等待隊列裏的最先一個線程轉移至同步隊列裏繼續等待,等真正輪到它了,纔會被喚醒。

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