06-J.U.C之Condition

1. 簡介

       在沒有Lock之前,我們使用synchronized來控制同步,配合Objectwait()notify()系列方法可以實現等待/通知模式。在JavaSE 5後,Java提供了Lock接口,相對於Synchronized而言,Lock提供了條件Condition,對線程的等待、喚醒操作更加詳細和靈活。

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

在這裏插入圖片描述

       Condition提供了一系列的方法來對阻塞和喚醒線程:

       1、await() :造成當前線程在接到信號或被中斷之前一直處於等待狀態。

       2、await(long time, TimeUnit unit):造成當前線程在接到信號、被中斷或到達指定等待時間之前一直處於等待狀態。

       3、awaitNanos(long nanosTimeout):造成當前線程在接到信號、被中斷或到達指定等待時間之前一直 處於等待狀態。返回值表示剩餘時間,如果在nanosTimesout之前喚醒,那麼返回值 = nanosTimeout - 消耗時間,如果返回值 <= 0 ,則可以認定它已經超時了。

       4、awaitUninterruptibly():造成當前線程在接到信號之前一直處於等待狀態。【注意:該方法對中斷不敏感】。

       5、awaitUntil(Date deadline):造成當前線程在接到信號、被中斷或到達指定最後期限之前一直處於等待狀態。如果沒有到指定時間就被通知,則返回true,否則表示到了指定時間,返回返回false

       6、signal():喚醒一個等待線程。該線程從等待方法返回前必須獲得與Condition相關的鎖。

       7、signalAll():喚醒所有等待線程。能夠從等待方法返回的線程必須獲得與Condition相關的鎖。

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

2. Condition的圖解

       在Object的監視器模型上,一個對象擁有一個同步隊列和等待隊列。而在併發包中的Lock(同步器)擁有一個同步隊列和多個等待隊列

在這裏插入圖片描述

3. Condition 的實現

       獲取一個Condition必須要通過LocknewCondition()方法。該方法定義在接口Lock下面,返回的結果是綁定到此 Lock 實例的新 Condition 實例。

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

public class ConditionObject implements Condition, java.io.Serializable {

}

3.1 等待隊列

       每個Condition對象都包含着一個FIFO隊列,該隊列是Condition對象通知/等待功能的關鍵。在隊列中每一個節點都包含着一個線程引用,該線程就是在該Condition對象上等待的線程。我們看Condition的定義就明白了:

public class ConditionObject implements Condition, java.io.Serializable {
    private static final long serialVersionUID = 1173984872572414699L;

    //頭節點
    private transient Node firstWaiter;
    //尾節點
    private transient Node lastWaiter;

    public ConditionObject() {
    }

    /** 省略方法 **/
}

       從上面代碼可以看出Condition擁有首節點(firstWaiter),尾節點(lastWaiter)。當前線程調用await()方法,將會以當前線程構造成一個節點(Node),並將節點加入到該隊列的尾部。結構如下:

在這裏插入圖片描述
       Node裏面包含了當前線程的引用。Node定義與AQSCLH同步隊列的節點使用的都是同一個類(AbstractQueuedSynchronized.Node靜態內部類)。Condition的隊列結構比CLH同步隊列的結構簡單些,新增過程較爲簡單隻需要將原尾節點的nextWaiter指向新增節點,然後更新lastWaiter即可。

       節點更新的過程並沒有使用CAS保證,原因在於調用await()方法的線程必定是獲取了鎖的線程,即該過程是由鎖來保證線程安全的

3.2 等待

       調用Conditionawait()方法會使當前線程進入等待狀態,同時會加入到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隊列中檢測到),如果不存在則一直掛起,否則參與競爭同步狀態。

       加入條件隊列(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條件隊列中。當然在加入到尾節點之前會清除所有狀態不爲Condition的節點

在這裏插入圖片描述
       fullyRelease(Node node),負責釋放該線程持有的鎖。

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

       isOnSyncQueue(Node node):如果一個節點剛開始在條件隊列上,現在在同步隊列上獲取鎖則返回true

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

    return findNodeFromTail(node);
}

       unlinkCancelledWaiters():負責將條件隊列中狀態不爲Condition的節點刪除。

private void unlinkCancelledWaiters() {
    Node t = firstWaiter;
    Node trail = null;
    while (t != null) {
        Node next = t.nextWaiter;
        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;
    }
}

3.3 通知

       調用Conditionsignal()方法,將會喚醒在等待隊列中等待最長時間的節點(條件隊列裏的首節點),在喚醒節點前,會將節點移到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);
}

在這裏插入圖片描述
       doSignal(Node first)主要是做兩件事:1.修改頭節點,2.調用transferForSignal(Node first)方法將節點移動到CLH同步隊列中。transferForSignal(Node first)源碼如下:

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

       整個通知的流程如下:

  1. 判斷當前線程是否已經獲取了鎖,如果沒有獲取則直接拋出異常,因爲獲取鎖爲通知的前置條件。
  2. 如果線程已經獲取了鎖,則將喚醒條件隊列的首節點
  3. 喚醒首節點是先將條件隊列中的頭節點移出,然後調用AQSenq(Node node)方法將其安全地移到CLH同步隊列中
  4. 最後判斷如果該節點的同步狀態是否爲Cancel,或者修改狀態爲Signal失敗時,則直接調用LockSupport喚醒該節點的線程。

4. 總結

       一個線程獲取鎖後,通過調用Conditionawait()方法,會將當前線程先加入到條件隊列中,然後釋放鎖,最後通過isOnSyncQueue(Nodenode)方法不斷自檢看節點是否已經在CLH同步隊列了,如果是則嘗試獲取鎖,否則一直掛起。當線程調用signal()方法後,程序首先檢查當前線程是否獲取了鎖,然後通過doSignal(Node first)方法喚醒CLH同步隊列的首節點。被喚醒的線程,將從await()方法中的while循環中退出來,然後調用acquireQueued()方法競爭同步狀態。

5. 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;
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章