condition實現解析 1.8

任意一個java對象,都擁有一組監視器方法(定義在java.lang.Object上),主要包括wait()、wait(long
timeOut)、notify()、notifyAll()方法,這些方法與synchronized同步關鍵字配合,可以實現等待/通知模式.condition接口也提供了類似Object的監視器方法,與Lock配合可以實現等待/通知模式,但是這倆者在使用方法以及功能特性上還是有差別的

以上摘自《java 併發編程的藝術》5.6節
在這裏插入圖片描述

condition相對於Object監視器方法最大的優勢在於, condition能設置多個等待隊列,這意味着利用condition能夠設計出更加靈活、性能更好的數據結構
例如:阻塞隊列ArrayBlockingQueue

	//基於lock接口的顯示鎖
    final ReentrantLock lock;

    //針對消費者的等待隊列
    private final Condition notEmpty;

    //針對生產者的等待隊列
    private final Condition notFull;

	public ArrayBlockingQueue(int capacity, boolean fair) {
        if (capacity <= 0)
            throw new IllegalArgumentException();
        this.items = new Object[capacity];
        //重點看這裏,通過同一個監視器鎖創建出倆個不同的等待隊列
        //這樣可以根據不同的條件判斷,來精準的使用具體的等待隊列,避免不必要的線程等待喚醒操作
        lock = new ReentrantLock(fair);
        notEmpty = lock.newCondition();
        notFull =  lock.newCondition();
    }

	//看下具體的使用,生產者生產對象
	public void put(E e) throws InterruptedException {
        checkNotNull(e);
        final ReentrantLock lock = this.lock;
        lock.lockInterruptibly();
        try {
        	//重點看這裏,如果判斷隊列已滿,那麼通知該生產者線程等待
            while (count == items.length)
                notFull.await();
            enqueue(e);
        } finally {
            lock.unlock();
        }
    }
	
	private void enqueue(E x) {
        final Object[] items = this.items;
        items[putIndex] = x;
        if (++putIndex == items.length)
            putIndex = 0;
        count++;
        //重點看這裏,精準的喚醒一個消費者線程,上面的生產者線程不受影響
        //如果使用notify()的話,並不知道喚醒的是生產者線程還是消費者線程
        //如果使用notifyAll()喚醒所有線程,又會造成大量的鎖競爭,資源浪費
        notEmpty.signal();
    }

接下來進入正題,看下condition是如何實現的,先來看下condition接口定義了哪些方法

//相當於Object.wait(),設置當前線程處於等待狀態,響應中斷,拋出InterruptedException
void await() throws InterruptedException;

//await 忽略中斷
void awaitUninterruptibly();

//await 加上超時機制,響應中斷,返回 nanosTimeout-消耗時間,如果返回<=0,說明已超時
long awaitNanos(long nanosTimeout) throws InterruptedException;

//await 加上超時機制,並且可以指定時間單位,響應中斷
boolean await(long time, TimeUnit unit) throws InterruptedException;

//await 指定時間界限,在此時間前被通知喚醒則返回true,否則爲false,響應中斷
boolean awaitUntil(Date deadline) throws InterruptedException;

//喚醒一個等待線程,前提是正在執行該方法的線程已經獲取了condtion相對應的鎖
void signal();

//喚醒所有等待線程
void signalAll();

AQS的內部類ConditionObject實現了condition接口

//condtion 和同步鎖隊列是共用同一個node對象
//condtion的等待隊列中存放了所有調用該condition.await 方法的線程
//當調用condtion.signal 方法,等待隊列中的節點將會挪到同步鎖隊列中
public class ConditionObject implements Condition, java.io.Serializable {
        //首節點
        private transient Node firstWaiter;
        //尾節點
        private transient Node lastWaiter;
}

//在看下node中 condtion特有的參數
static final class Node {
	//針對於condtion節點,初始狀態爲CONDITION
    static final int CONDITION = -2;
	
	//和同步器共用的一個參數,在同步器中代表node的模式(獨佔、共享)
	//condtion中表示指向下一個節點,condtion是一個單向鏈表
	volatile int waitStatus;
}

下面通過圖示來表達同步鎖隊列和 等待隊列之間的關聯

在這裏插入圖片描述

看具體的源碼await

		//主要邏輯
        //1、在condition隊列中添加一個節點,執行Release釋放自身同步狀態
        //2、循環判斷該節點是否被轉移到鎖同步隊列,如果沒有則掛起等待
        //3、如果被中斷,則跳出2的循環做出相應響應(這個響應要在重新獲取同步狀態後才能做)
        //第一個過程是在獲取同步狀態的情況下執行,所以不用考慮併發情況,
        //後倆個過程因爲已經釋放同步狀態了,需要考慮併發
        public final void await() throws InterruptedException {
            if (Thread.interrupted())
                throw new InterruptedException();
            //在condition隊列中添加一個節點,狀態是condition
            Node node = addConditionWaiter();
            //執行release,如果失敗了,拋出IllegalMonitorStateException
            //如果這裏成功,則表示該線程已經成功釋放了同步狀態,下面的流程都需要考慮併發情況了
            int savedState = fullyRelease(node);

            //這裏初始狀態爲0
            //1、當該節點被移到同步鎖隊列之前中斷(signal之前),則interruptMode爲THROW_IE,最終會拋出InterruptedException
            //2、當該節點在移動同步鎖隊列之後中斷(signal之後),則interruptMode爲REINTERRUPT,保留interrupt狀態
            //3、如果沒有被中斷,保持原有狀態0
            int interruptMode = 0;
            //循環判斷該節點是否被移動到同步鎖隊列,如果沒有則掛起等待
            while (!isOnSyncQueue(node)) {
                LockSupport.park(this);
                //如果是被中斷了,則將節點轉移到同步鎖隊列,同時跳出循環
                if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
                    break;
            }
            //進行到這裏說明節點已經被轉移到同步鎖隊列了,執行acquireQueued
            if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
                interruptMode = REINTERRUPT;
            //執行到這裏,說明acquire成功,已經重新獲取了同步狀態
            if (node.nextWaiter != null) // clean up if cancelled
                //清理condtion等待隊列中的CANCELLED節點
                unlinkCancelledWaiters();
            //如果不爲0,說明之前該線程中斷過,那麼做出相應的響應
            if (interruptMode != 0)
                reportInterruptAfterWait(interruptMode);
        }
		

	//判斷該節點在同步鎖隊列中是否存在
	final boolean isOnSyncQueue(Node node) {
		//當狀態爲CONDITION時,或者當pre(同步器隊列用到的前驅)爲null,則說明不存在
        if (node.waitStatus == Node.CONDITION || node.prev == null)
            return false;
        //next是同步器用到的後繼節點,如果不爲空,那麼該節點就肯定在同步鎖隊列了
        //這裏很有趣,下面會具體分析爲什麼沒有node.prev!=null
        if (node.next != null) 
            return true;
        //從同步鎖隊列尾部查找是否有這個節點    
        return findNodeFromTail(node);
    }

	

isOnSyncQueue方法有個很有趣的地方,if (node.next != null) 可以判斷節點肯定已經在同步鎖隊列了,那麼同理node.prev != null 是不是也能證明呢
答案當然不是了,看以下代碼

//這個方法是aqs往同步鎖隊列添加節點
private Node enq(final Node node) {
        for (;;) {
            Node t = tail;
            //如果未初始化隊列,創建head,cas賦值
            if (t == null) { // Must initialize
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {
             //重點在這裏,是先把該節點的先驅和尾節點關聯上,再使用cas
        	//如果這個過程中cas失效了,那麼該節點就添加失敗了,那這個關聯當然也就是無效的
        	//而signal時會調用這個方法,所以不能通過node的pre是否爲null來判斷節點是否已經成功移到同步鎖隊列
                node.prev = t;
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
    }

其中還有一個比較重要的方法是checkInterruptWhileWaiting,判斷線程有沒有收到中斷訊號、中斷的時機(signal前還是signal後)

	private int checkInterruptWhileWaiting(Node node) {
            //當檢測到線程中斷信號則執行transferAfterCancelledWait,否則返回0
            //transferAfterCancelledWait邏輯主要判斷中斷的時機
            return Thread.interrupted() ?
                (transferAfterCancelledWait(node) ? THROW_IE : REINTERRUPT) :
                0;
        }
        
        
   final boolean transferAfterCancelledWait(Node node) {
        //如果cas修改狀態成功了,說明此時還沒有收到signal信號
        if (compareAndSetWaitStatus(node, Node.CONDITION, 0)) {
            //將該節點轉移到同步鎖隊列
            //需要注意的是,雖然執行到這裏註定了該線程會被拋出InterruptedException
            //但是也要等到獲取同步狀態之後才能拋出,否則會產生線程安全問題
            enq(node);
            return true;
        }
        //上面cas失敗說明已經收到signal信號,singnal方法已經執行
        //但是也有可能沒執行完,也就是節點還正在轉移過程中,所以進行自旋等待
        //返回false表明該線程只會記錄中斷狀態,不會拋錯
        while (!isOnSyncQueue(node))
            Thread.yield();
        return false;
    }

到這裏await基本講完了,awaitUninterruptibly、awaitNanos原理基本一樣就不多描述了,接下來看看signal

	public final void signal() {
			//這裏需要注意,isHeldExclusively是一個空方法,需要用戶自己判斷正在執行的線程是否已經獲得同步
            if (!isHeldExclusively())
                throw new IllegalMonitorStateException();
            Node first = firstWaiter;
            if (first != null)
                doSignal(first);
    }

	//需用用戶繼承重寫,一般的邏輯就是判斷當前線程是否獲得同步狀態,
	//這樣接下來的操作就是線程安全的,否則會帶來線程安全隱患
	protected boolean isHeldExclusively() {
        throw new UnsupportedOperationException();
    }


	
	private void doSignal(Node first) {
            do {
            	//頭節點出隊,重新設置nextWaiter爲頭節點
                if ( (firstWaiter = first.nextWaiter) == null)
                	//如果隊列爲空,設置lastWaiter爲null
                    lastWaiter = null;
                //原頭節點斷開後繼關聯
                first.nextWaiter = null;
        	//transferForSignal失敗表示該節點signal失敗,繼續下一個節點
            } while (!transferForSignal(first) &&
                     (first = firstWaiter) != null);
     }


	final boolean transferForSignal(Node node) {
        //如果這裏cas失敗,表示這個節點狀態是cancel,那麼直接返回找下一個節點
        if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
            return false;
        //將該節點放入同步鎖隊列
        //注意,這裏返回的p是node的前驅節點
        Node p = enq(node);
        int ws = p.waitStatus;
        //如果前驅節點是cancel,或者設置SIGNAL失敗,則喚醒本節點處理
        if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
            LockSupport.unpark(node.thread);
        return true;
    }

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