Java併發程式設計 -- Condition

微笑面對生活發表於2018-08-26

因為wait()、notify()是和synchronized配合使用的,因此如果使用了顯示鎖Lock,就不能用了。所以顯示鎖要提供自己的等待/通知機制,Condition應運而生。

Condition中的await()方法相當於Object的wait()方法,Condition中的signal()方法相當於Object的notify()方法,Condition中的signalAll()相當於Object的notifyAll()方法。不同的是,Object中的wait(),notify(),notifyAll()方法是和"同步鎖"(synchronized關鍵字)捆綁使用的;而Condition是需要與"互斥鎖"/"共享鎖"捆綁使用的。

1. 函式列表

  1. void await() throws InterruptedException 當前執行緒進入等待狀態,直到被通知(signal)或者被中斷時,當前執行緒進入執行狀態,從await()返回;

  2. void awaitUninterruptibly() 當前執行緒進入等待狀態,直到被通知,對中斷不做響應;

  3. long awaitNanos(long nanosTimeout) throws InterruptedException 在介面1的返回條件基礎上增加了超時響應,返回值表示當前剩餘的時間,如果在nanosTimeout之前被喚醒,返回值 = nanosTimeout - 實際消耗的時間,返回值 <= 0表示超時;

  4. boolean await(long time, TimeUnit unit) throws InterruptedException 同樣是在介面1的返回條件基礎上增加了超時響應,與介面3不同的是: 可以自定義超時時間單位; 返回值返回true/false,在time之前被喚醒,返回true,超時返回false。

  5. boolean awaitUntil(Date deadline) throws InterruptedException 當前執行緒進入等待狀態直到將來的指定時間被通知,如果沒有到指定時間被通知返回true,否則,到達指定時間,返回false;

  6. void signal() 喚醒一個等待在Condition上的執行緒;

  7. void signalAll() 喚醒等待在Condition上所有的執行緒。

2. 具體實現

看到電腦上當初有幫人做過一道題,就拿它做例項演示:

編寫一個Java應用程式,要求有三個程式:student1,student2,teacher,其中執行緒student1準備“睡”1分鐘後再開始上課,執行緒student2準備“睡”5分鐘後再開始上課。Teacher在輸出4句“上課”後,“喚醒”了休眠的執行緒student1;執行緒student1被“喚醒”後,負責再“喚醒”休眠的執行緒student2.

2.1 實現一:

基於Object和synchronized實現。

package com.fantJ.bigdata;

/**
 * Created by Fant.J.
 * 2018/7/2 16:36
 */
public class Ten {
    static class Student1{
        private boolean student1Flag = false;
        public synchronized boolean isStudent1Flag() {
            System.out.println("學生1開始睡覺1min");
            if (!this.student1Flag){
                try {
                    System.out.println("學生1睡著了");
                    wait(1*1000*60);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("學生1被喚醒");

            return student1Flag;
        }

        public synchronized void setStudent1Flag(boolean student1Flag) {
            this.student1Flag = student1Flag;
            notify();
        }
    }
    static class Student2{
        private boolean student2Flag = false;
        public synchronized boolean isStudent2Flag() {
            System.out.println("學生2開始睡覺5min");
            if (!this.student2Flag){
                try {
                    System.out.println("學生2睡著了");
                    wait(5*1000*60);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("學生2被喚醒");
            return student2Flag;
        }

        public synchronized void setStudent2Flag(boolean student2Flag) {
            notify();
            this.student2Flag = student2Flag;
        }
    }
    static class Teacher{
        private boolean teacherFlag = true;
        public synchronized boolean isTeacherFlag() {
            if (!this.teacherFlag){
                try {
                    wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("老師準備吼著要上課");

            return teacherFlag;
        }

        public synchronized void setTeacherFlag(boolean teacherFlag) {
            this.teacherFlag = teacherFlag;
            notify();
        }
    }
    public static void main(String[] args) {
        Student1 student1 = new Student1();
        Student2 student2 = new Student2();
        Teacher teacher = new Teacher();

        Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0;i<4;i++){
                    System.out.println("上課");
                }
                teacher.isTeacherFlag();
                System.out.println("學生1被吵醒了,1s後反應過來");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                student1.setStudent1Flag(true);
            }
        });
        Thread s1 = new Thread(new Runnable() {
            @Override
            public void run() {
                student1.isStudent1Flag();
                System.out.println("準備喚醒學生2,喚醒需要1s");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                student2.setStudent2Flag(true);
            }
        });
        Thread s2 = new Thread(new Runnable() {
            @Override
            public void run() {
                student2.isStudent2Flag();
            }
        });

        s1.start();
        s2.start();
        t.start();
    }
}
複製程式碼

當然,用notifyAll可能會用更少的程式碼,這種實現方式雖然複雜,單效能上會比使用notifyAll()要強很多,因為沒有鎖爭奪導致的資源浪費。但是可以看到,程式碼很複雜,例項與例項之間也需要保證很好的隔離。

2.2 實現二:

基於Condition、ReentrantLock實現。

public class xxx{
        private int signal = 0;
        public Lock lock = new ReentrantLock();
        Condition teacher = lock.newCondition();
        Condition student1 = lock.newCondition();
        Condition student2 = lock.newCondition();

        public void teacher(){
            lock.lock();
            while (signal != 0){
                try {
                    teacher.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("老師叫上課");
            signal++;
            student1.signal();
            lock.unlock();
        }
        public void student1(){
            lock.lock();
            while (signal != 1){
                try {
                    student1.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("學生1醒了,準備叫醒學生2");
            signal++;
            student2.signal();
            lock.unlock();
        }
        public void student2(){
            lock.lock();
            while (signal != 2){
                try {
                    student2.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("學生2醒了");
            signal=0;
            teacher.signal();
            lock.unlock();
        }

        public static void main(String[] args) {
            ThreadCommunicate2 ten = new ThreadCommunicate2();
            new Thread(() -> ten.teacher()).start();
            new Thread(() -> ten.student1()).start();
            new Thread(() -> ten.student2()).start();
        }
}
複製程式碼

Condition依賴於Lock介面,生成一個Condition的基本程式碼是lock.newCondition() 呼叫Conditionawait()signal()方法,都必須在lock保護之內,就是說必須在lock.lock()lock.unlock之間才可以使用。

可以觀察到,我取消了synchronized方法關鍵字,在每個加鎖的方法前後分別加了lock.lock(); lock.unlock();來獲取/施放鎖,並且在釋放鎖之前施放想要施放的Condition物件。同樣的,我們使用signal來完成執行緒間的通訊。

3. Condition實現有界佇列

為什麼要用它來實現有界佇列呢,因為我們可以利用Condition來實現阻塞(當佇列空或者滿的時候)。這就為我們減少了很多的麻煩。

public class MyQueue<E> {

    private Object[] objects;
    private Lock lock = new ReentrantLock();
    private Condition addCDT = lock.newCondition();
    private Condition rmCDT = lock.newCondition();

    private int addIndex;
    private int rmIndex;
    private int queueSize;

    MyQueue(int size){
        objects = new Object[size];
    }
    public void add(E e){
        lock.lock();
        while (queueSize == objects.length){
            try {
                addCDT.await();
            } catch (InterruptedException e1) {
                e1.printStackTrace();
            }
        }
        objects[addIndex] = e;
        System.out.println("新增了資料"+"Objects["+addIndex+"] = "+e);
        if (++addIndex == objects.length){
            addIndex = 0;
        }
        queueSize++;
        rmCDT.signal();
        lock.unlock();

    }
    public Object remove(){
        lock.lock();
        while (queueSize == 0){
            try {
                System.out.println("佇列為空");
                rmCDT.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        Object temp = objects[rmIndex];
        objects[rmIndex] = null;
        System.out.println("移除了資料"+"Objects["+rmIndex+"] = null");
        if (++rmIndex == objects.length){
            rmIndex = 0;
        }
        queueSize--;
        addCDT.signal();
        lock.unlock();
        return temp;
    }
    public void foreach(E e){
        if (e instanceof String){
            Arrays.stream(objects).map(obj->{
                if (obj == null){
                    obj = " ";
                }
                return obj;
            }).map(Object::toString).forEach(System.out::println);
        }
        if (e instanceof Integer){
            Arrays.stream(objects).map(obj -> {
                if (obj == null ){
                    obj = 0;
                }
                return obj;
            }).map(object -> Integer.valueOf(object.toString())).forEach(System.out::println);
        }
    }
}
複製程式碼

add 方法就是往佇列中新增資料。 remove 是從佇列中按FIFO移除資料。 foreach 方法是一個觀察佇列內容的工具方法,很容易看出,它是用來遍歷的。

    public static void main(String[] args) {
        MyQueue<Integer> myQueue = new MyQueue<>(5);
        myQueue.add(5);
        myQueue.add(4);
        myQueue.add(3);
//        myQueue.add(2);
//        myQueue.add(1);
        myQueue.remove();
        myQueue.foreach(5);
    }
複製程式碼
新增了資料Objects[0] = 5
新增了資料Objects[1] = 4
新增了資料Objects[2] = 3
移除了資料Objects[0] = null
0
4
3
0
0
複製程式碼

4. 原始碼分析

ReentrantLock.class

public Condition newCondition() {
    return sync.newCondition();
}
複製程式碼

sync溯源:

private final Sync sync;
複製程式碼

Sync類中有一個newCondition()方法:

final ConditionObject newCondition() {
    return new ConditionObject();
}
複製程式碼
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 interface Condition {
    void await() throws InterruptedException;
    void awaitUninterruptibly();
    long awaitNanos(long nanosTimeout) throws InterruptedException;
    boolean await(long time, TimeUnit unit) throws InterruptedException;
    boolean awaitUntil(Date deadline) throws InterruptedException;
    void signal();
    void signalAll();
複製程式碼
await原始碼:
public final void await() throws InterruptedException {
    // 1.如果當前執行緒被中斷,則丟擲中斷異常
    if (Thread.interrupted())
        throw new InterruptedException();
    // 2.將節點加入到Condition佇列中去,這裡如果lastWaiter是cancel狀態,那麼會把它踢出Condition佇列。
    Node node = addConditionWaiter();
    // 3.呼叫tryRelease,釋放當前執行緒的鎖
    long savedState = fullyRelease(node);
    int interruptMode = 0;
    // 4.為什麼會有在AQS的等待佇列的判斷?
    // 解答:signal操作會將Node從Condition佇列中拿出並且放入到等待佇列中去,在不在AQS等待佇列就看signal是否執行了
    // 如果不在AQS等待佇列中,就park當前執行緒,如果在,就退出迴圈,這個時候如果被中斷,那麼就退出迴圈
    while (!isOnSyncQueue(node)) {
        LockSupport.park(this);
        if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
            break;
    }
    // 5.這個時候執行緒已經被signal()或者signalAll()操作給喚醒了,退出了4中的while迴圈
    // 自旋等待嘗試再次獲取鎖,呼叫acquireQueued方法
    if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
        interruptMode = REINTERRUPT;
    if (node.nextWaiter != null)
        unlinkCancelledWaiters();
    if (interruptMode != 0)
        reportInterruptAfterWait(interruptMode);
}
複製程式碼
  1. 將當前執行緒加入Condition鎖佇列。特別說明的是,這裡不同於AQS的佇列,這裡進入的是ConditionFIFO佇列。 

  2. 釋放鎖。這裡可以看到將鎖釋放了,否則別的執行緒就無法拿到鎖而發生死鎖。 

  3. 自旋(while)掛起,直到被喚醒(signal把他重新放回到AQS的等待佇列)或者超時或者CACELLED等。 

  4. 獲取鎖(acquireQueued)。並將自己從ConditionFIFO佇列中釋放,表明自己不再需要鎖(我已經拿到鎖了)。

signal()原始碼
public final void signal() {
            if (!isHeldExclusively())
              //如果同步狀態不是被當前執行緒獨佔,直接丟擲異常。從這裡也能看出來,Condition只能配合獨佔類同步元件使用。
                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) &&   //transferForSignal方法嘗試喚醒當前節點,如果喚醒失敗,則繼續嘗試喚醒當前節點的後繼節點。
                     (first = firstWaiter) != null);
        }

    final boolean transferForSignal(Node node) {
        //如果當前節點狀態為CONDITION,則將狀態改為0準備加入同步佇列;如果當前狀態不為CONDITION,說明該節點等待已被中斷,則該方法返回falsedoSignal()方法會繼續嘗試喚醒當前節點的後繼節點
        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);  //將節點加入同步佇列,返回的p是節點在同步佇列中的先驅節點
        int ws = p.waitStatus;
        //如果先驅節點的狀態為CANCELLED(>0) 或設定先驅節點的狀態為SIGNAL失敗,那麼就立即喚醒當前節點對應的執行緒,執行緒被喚醒後會執行acquireQueued方法,該方法會重新嘗試將節點的先驅狀態設為SIGNAL並再次park執行緒;如果當前設定前驅節點狀態為SIGNAL成功,那麼就不需要馬上喚醒執行緒了,當它的前驅節點成為同步佇列的首節點且釋放同步狀態後,會自動喚醒它。
        //其實筆者認為這裡不加這個判斷條件應該也是可以的。只是對於CAS修改前驅節點狀態為SIGNAL成功這種情況來說,如果不加這個判斷條件,提前喚醒了執行緒,等進入acquireQueued方法了節點發現自己的前驅不是首節點,還要再阻塞,等到其前驅節點成為首節點並釋放鎖時再喚醒一次;而如果加了這個條件,執行緒被喚醒的時候它的前驅節點肯定是首節點了,執行緒就有機會直接獲取同步狀態從而避免二次阻塞,節省了硬體資源。
        if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
            LockSupport.unpark(node.thread);
        return true;
    }
複製程式碼

signal就是喚醒Condition佇列中的第一個非CANCELLED節點執行緒,而signalAll就是喚醒所有非CANCELLED節點執行緒,本質是將節點從Condition佇列中取出來一個還是所有節點放到AQS的等待佇列。儘管所有Node可能都被喚醒,但是要知道的是仍然只有一個執行緒能夠拿到鎖,其它沒有拿到鎖的執行緒仍然需要自旋等待,就上上面提到的第4步(acquireQueued)。

實現過程概述

我們知道Lock的本質是AQSAQS自己維護的佇列是當前等待資源的佇列,AQS會在資源被釋放後,依次喚醒佇列中從前到後的所有節點,使他們對應的執行緒恢復執行,直到佇列為空。而Condition自己也維護了一個佇列,該佇列的作用是維護一個等待signal訊號的佇列。但是,兩個佇列的作用不同的,事實上,每個執行緒也僅僅會同時存在以上兩個佇列中的一個,流程是這樣的:

  1. 執行緒1呼叫reentrantLock.lock時,嘗試獲取鎖。如果成功,則返回,從AQS的佇列中移除執行緒;否則阻塞,保持在AQS的等待佇列中。
  2. 執行緒1呼叫await方法被呼叫時,對應操作是被加入到Condition的等待佇列中,等待signal訊號;同時釋放鎖。
  3. 鎖被釋放後,會喚醒AQS佇列中的頭結點,所以執行緒2會獲取到鎖。
  4. 執行緒2呼叫signal方法,這個時候Condition的等待佇列中只有執行緒1一個節點,於是它被取出來,並被加入到AQS的等待佇列中。注意,這個時候,執行緒1 並沒有被喚醒,只是被加入AQS等待佇列。
  5. signal方法執行完畢,執行緒2呼叫unLock()方法,釋放鎖。這個時候因為AQS中只有執行緒1,於是,執行緒1被喚醒,執行緒1恢復執行。 所以: 傳送signal訊號只是將Condition佇列中的執行緒加到AQS的等待佇列中。只有到傳送signal訊號的執行緒呼叫reentrantLock.unlock()釋放鎖後,這些執行緒才會被喚醒。

可以看到,整個協作過程是靠結點在AQS的等待佇列和Condition的等待佇列中來回移動實現的,Condition作為一個條件類,很好的自己維護了一個等待訊號的佇列,並在適時的時候將結點加入到AQS的等待佇列中來實現的喚醒操作。

Condition等待通知的本質請參考:www.cnblogs.com/sheeva/p/64…

相關文章