JUC源碼解析-阻塞隊列-迭代器(一)

本篇來分析下阻塞隊列裏迭代器的實現,以ArrayBlockingQueue源碼來分析。

首先在開始前想一想,如何實現阻塞隊列的迭代器功能?

在併發下有些線程在讀,有些在改,還有些在使用迭代器遍歷,怎麼確保安全性?用獨佔鎖將這些操作隔離開,我們看 ArrayBlockingQueue 確實是這麼做的。

既然安全性得到保障那麼還有什麼問題是需要考慮的 ?
過時數據問題。假設一個線程從此時的 takeIndex 位置開始使用迭代器往後遍歷,每此調用 next 獲取下一個數據的操作都需要提前獲取鎖,有可能很長時間都獲取不到鎖,這段有其它線程在讀取本質上就是增加 takeIndex,那麼當輪到這個迭代線程執行時他就應該首先檢查下情況,是否自己接下來要讀取的內容已經失效了(即位置下標落後於此時 takeIndex值)。還有刪除數據時也要考慮到被刪除位置對多有迭代器的影響。

併發下不只一個線程在使用迭代器,每個使用者都會創建 Itr 對象,它們構成一條鏈,Itrs 管理這條鏈。

迭代器 Itr

當你調用 iterator() 方法時,創建迭代器對象 Itr;

    public Iterator<E> iterator() {
        return new Itr();
    }

1,重要的變量

先來介紹下迭代器 Itr 中非常重要的變量:

		/** Index to look for new nextItem; NONE at end */
        private int cursor;

        /** Element to be returned by next call to next(); null if none */
        private E nextItem;

        /** Index of nextItem; NONE if none, REMOVED if removed elsewhere */
        private int nextIndex;

註解很清楚的解釋了三者的功能,每此 next() 調用返回的就是 nextItem 對象,而 nextIndex 指的是nextItem對象的下標位置,cursor 是 nextIndex 的下一個位置。在不斷 next 獲取元素過程中,這三者就是兩個在前一個在後的這麼往下移動着。

        /** Last element returned; null if none or not detached. */
        private E lastItem;

        /** Index of lastItem, NONE if none, REMOVED if removed elsewhere */
        private int lastRet;

lastItem 表示上一次返回的元素;lastRet 就是 lastItem 的下標。
我這裏稱 cursor,nextIndex,lastRet 爲迭代過程的三劍客,當滿足 cursor < 0 && nextIndex < 0 && lastRet < 0 時代表迭代器失效。

        /** Previous value of takeIndex, or DETACHED when detached */
        private int prevTakeIndex;

        /** Previous value of iters.cycles */
        private int prevCycles;

這兩個變量與處理過時數據問題有關。

prevTakeIndex 代表本次遍歷開始的位置,每此 next 都會進行修正;
prevCycles :Itrs 管理 Itr 鏈,它裏面有個變量 cycles 記錄 takeIndex 回到 0 位置的次數,迭代器的 prevCycles 存儲該值。

迭代器操作需要獲取獨佔鎖,得不到就得等待,這就造成了其存儲的 prevTakeIndex 與 prevCycles 可能過時,迭代器多處操作前都會通過這兩個值來判斷數據是否過時,以做相應的處理。

        /** Special index value indicating "not available" or "undefined" */
        private static final int NONE = -1;

        /**
         * Special index value indicating "removed elsewhere", that is,
         * removed by some operation other than a call to this.remove().
         */
        private static final int REMOVED = -2;

        /** Special value for prevTakeIndex indicating "detached mode" */
        private static final int DETACHED = -3;

DETACHED:專門用於preTakeIndex,isDetached方法通過其來判斷迭代器狀態
NONE:用於三個下標變量:cursor,nextIndex,lastRet;這三個下標變量用於迭代功能的實現。表明該位置數據不可用或未定義。
REMOVED:用於lastRet 與 nextIndex。表明數據過時或被刪除。

接下來結合具體實現來看:

2,初始化

Itr 初始化過程:

    private class Itr implements Iterator<E> {
    
            Itr() {
            // assert lock.getHoldCount() == 0;
            lastRet = NONE;
            final ReentrantLock lock = ArrayBlockingQueue.this.lock;
            lock.lock(); // 將對迭代器鏈的操作及變量的操作用鎖保護起來
            try {
            	//數組爲空
                if (count == 0) {
                    // assert itrs == null;
                    cursor = NONE;
                    nextIndex = NONE;
                    prevTakeIndex = DETACHED;
                } else {
                	// 初始時prevTakeIndex等於takeIndex
                	// nextIndex 等於 takeIndex
                	// nextItem 爲takeIndex位置的元素
                	// cursor 爲takeIndex的後一個位置
                    final int takeIndex = ArrayBlockingQueue.this.takeIndex;
                    prevTakeIndex = takeIndex;
                    nextItem = itemAt(nextIndex = takeIndex);
                    cursor = incCursor(takeIndex);
                    if (itrs == null) {
                        itrs = new Itrs(this);
                    } else {
                        itrs.register(this); // in this order
                        itrs.doSomeSweeping(false);
                    }
                    prevCycles = itrs.cycles;
                    // assert takeIndex >= 0;
                    // assert prevTakeIndex == takeIndex;
                    // assert nextIndex >= 0;
                    // assert nextItem != null;
                }
            } finally {
                lock.unlock();
            }
        }

可以看出初始化幹了三件事:將自身加入迭代器鏈 , 初始化變量 , 清掃迭代器鏈。

2.1,初始化變量

  • prevTakeIndex等於 takeIndex,記錄本次迭代開始的位置,每此next都會對其進行更新。
  • prevCycles = itrs.cycles,用於判斷接下來數據是否過時。
  • nextIndex 等於 takeIndex,這三個變量都是迭代過程中使用到的。
  • nextItem 爲takeIndex位置的元素
  • cursor 爲takeIndex的後一個位置

關於 cursor 的增加操作

        private int incCursor(int index) {
            // assert lock.getHoldCount() == 1;
            if (++index == items.length)
                index = 0;
            if (index == putIndex)
                index = NONE;
            return index;
        }

當遍歷到 putIndex ,代表數據遍歷結束,應該終止迭代,將 cursor 置爲 NONE 標識,cursor 置爲 NONE 後會引起迭代器的終止,邏輯在 next 與 hasNext 方法中。

2.2,註冊

        private Node head; // 標識迭代器鏈的頭節點
        
        void register(Itr itr) {
            // assert lock.getHoldCount() == 1;
            head = new Node(itr, head);
        }
        
        private class Node extends WeakReference<Itr> {
            Node next;

            Node(Itr iterator, Node next) {
                super(iterator);
                this.next = next;
            }
        }

將Node對 Itr 對象的引用設置爲弱引用。當迭代線程結束迭代後就只會剩下Node 對 Itr 的弱引用,當 GC 啓動後會回收該對象,通過 get 的返回來判斷是否被回收。對於迭代器對象被回收的節點會被從迭代器鏈中刪除。

2.3,清掃迭代器鏈 doSomeSweeping

清掃方法 doSomeSweeping 並非一次將整個鏈條探測一遍,開始時選擇探測的範圍4或16,若在範圍內未探測到無效迭代器則結束,若是探測到則擴大探測範圍,將範圍恢復爲16,繼續往下探測。

先來介紹 Itrs 中的三個相關變量

		//記錄上次探測的結束位置節點,下次從此開始往後探測。
        private Node sweeper = null;
		// 探測範圍
        private static final int SHORT_SWEEP_PROBES = 4;
        private static final int LONG_SWEEP_PROBES = 16;

doSomeSweeping 將清除迭代器鏈中的無效節點,所謂無效指的是:1,節點持有的 Itr 對象爲空,說明被GC回收,能被回收也說明使用它的線程完成了迭代。2,迭代器 Itr 此時是 DETACHED 模式,對於迭代結束的迭代器會被置於 DETACHED 模式。迭代結束指的是迭代到 cursor 等於 putIndex,標誌着全部數據迭代完畢。

doSomeSweeping 清掃迭代鏈但並非從頭到尾全部探測一遍。之所以有這種設計我想是爲了避免耗時,畢竟此時迭代線程持有着獨佔鎖。

        void doSomeSweeping(boolean tryHarder) {
            // assert lock.getHoldCount() == 1;
            // assert head != null;

			//tryHarder 爲true則 probes 爲 16,否則爲 4
			// probes 代表本次的探測長度,所以你明白爲啥取名叫tryHader了
            int probes = tryHarder ? LONG_SWEEP_PROBES : SHORT_SWEEP_PROBES;
            Node o, p; // o 代表 p 的前一個節點,用於鏈表中節點的刪除
            // 它代表了一次探測中到達的最後一個節點
            final Node sweeper = this.sweeper;
            // 限制最多隻遍歷一遍
            // 若本次遍歷從半道開始不是從頭開始,當其遍歷到尾部但probes仍>0
            // 則轉到頭節點繼續;但若是從頭開始的遍歷,遍歷到尾probes>0,沒必要再繼續,應該終止遍歷。
            // 該功能的實現靠的就是 passedGo
            boolean passedGo;
			// 從頭開始的遍歷,passedGo 設爲true,在遇到鏈表長度小於probes的情況,能夠break終止
            if (sweeper == null) {
                o = null;
                p = head;
                passedGo = true;
            // 從前一個線程終止的位置開始
            } else {
                o = sweeper;
                p = o.next;
                passedGo = false;
            }

            for (; probes > 0; probes--) {
                if (p == null) {
                    if (passedGo) // 這就是passedGo發揮作用的地方
                        break;
                    //passedGo爲false,說明本次遍歷是從中間某位置開始,
                    //也就是說鏈表前面有一段是未遍歷的,
                    //遍歷到了尾部需要轉回到頭部繼續遍歷
                    o = null;
                    p = head;
                    passedGo = true;
                }
                final Itr it = p.get();
                final Node next = p.next;
                //節點持有的迭代器對象爲null,或是數組爲空或數據過時導致的DETACHED模式,
                //則刪除此節點
                if (it == null || it.isDetached()) {
                    //當發現了一個被拋棄或過時的迭代器,
                    //則將探測範圍probes變爲16,相當於延長探測範圍。
                    //這就是探測的本意吧:找不到就按原先的探測範圍直到結束,
                    //若找到了一個就擴大探測範圍繼續往下找。
                    
                    //以上的設計可能原因是:無論是由於 被回收 或是 過時
                    // 發現一個則之後的節點都極有可能存在相同的情況
                    probes = LONG_SWEEP_PROBES; // "try harder"
                    // unlink p
                    p.clear();
                    p.next = null;
                    if (o == null) {
                        head = next;
                        if (next == null) {
                            // We've run out of iterators to track; retire
                            itrs = null;
                            return;
                        }
                    }
                    else
                        o.next = next;
                } else {
                    o = p;
                }
                p = next;
            }
			// 記錄本次遍歷結束位置節點
            this.sweeper = (p == null) ? null : o;
        }

3,迭代操作hasNext與next

3.1,hasNext

        public boolean hasNext() {
            // assert lock.getHoldCount() == 0;
            if (nextItem != null)
                return true;
            noNext();
            return false;
        }

nextItem != null 說明仍有元素可以繼續遍歷,nextItem 代表next方法的返回值。當沒有元素可繼續遍歷時調用 noNext 方法。

noNext
隨着 next 的不斷調用,cursor 隨之增加,最後 cursor 等於 putIndex,表明數組元素全部遍歷完,這之後下標變量變爲 cursor = NONE ,nextIndex = NONE , nextItem = null,邏輯再next方法中。到這裏直接退出不就行了 ? 不行,需要將迭代器的狀態置爲 DETACHED,這樣才能被 doSomeSweeping 方法清除。這就是 noNext 方法實現的功能:將迭代器狀態置爲 DETACHED

        private void noNext() {
            final ReentrantLock lock = ArrayBlockingQueue.this.lock;
            lock.lock();
            try {
            	//調用該方法時cursor與nextIndex皆爲NONE,邏輯在next方法中
                // assert cursor == NONE;
                // assert nextIndex == NONE;
                
                // detach可能在最後一次next中被調用,所以這裏先進行判斷
                if (!isDetached()) {
                	//lastRet記錄前一個next返回的元素的下標,
                	//cursor等於putIndex,迭代結束,方法運行到此
                	//此時的lastRet一定是>= 0的
                	
                    // assert lastRet >= 0; 

					//該方法處理數據過時問題,會修正下標變量或直接detach該迭代器
					//那麼爲什麼在這裏調用該方法,有必要嗎?
					//因爲我們要對 lastItem 進行賦值,可若數據過時了便沒有這個必要了
					//所以這裏調用該方法,若過時則lastRet被置爲REMOVED小於0,還會調用detach方法
                    incorporateDequeues(); // might update lastRet
                    if (lastRet >= 0) {
                        lastItem = itemAt(lastRet);
                        // assert lastItem != null;
                        detach(); // 調用detach
                    }
                }
                // assert isDetached();迭代器處於了DETACHED狀態
                // assert lastRet < 0 ^ lastItem != null;相當於lastRet < 0 && lastItem == null
            } finally {
                lock.unlock();
            }
        }

首先代碼的執行是需要先獲取鎖的。
從代碼中可以看出detach一定會被調用,隱藏在incorporateDequeues中或是直接調用detach。還有需要注意的是代碼中聲明的幾個assert,理解他們有助於理解代碼的設計。

3.2,next

        public E next() {
            // assert lock.getHoldCount() == 0;
            final E x = nextItem;
            if (x == null)
                throw new NoSuchElementException();
            final ReentrantLock lock = ArrayBlockingQueue.this.lock;
            lock.lock();
            try {
                if (!isDetached())
                	//修正下標,主要是cursor,確保其在當前takeIndex之後(包括等於takeIndex)
                	//從而保證返回的元素不是過時的數據
                    incorporateDequeues();
                // assert nextIndex != NONE;
                // assert lastItem == null;
                lastRet = nextIndex;
                final int cursor = this.cursor;
                if (cursor >= 0) {
                    nextItem = itemAt(nextIndex = cursor);
                    // assert nextItem != null;
                    this.cursor = incCursor(cursor);
                } else {
                    nextIndex = NONE;
                    nextItem = null;
                }
            } finally {
                lock.unlock();
            }
            return x;
        }

重要的有兩點:1,需要獲取鎖。2,獲取前修正相關下標變量的值。用獨佔鎖保證了安全,可也造成了獲取元素前檢查數組狀態的必要性。

incorporateDequeues方法
該方法主要作用是:修正下標,保證返回數據的有效性。
由於多線程下爲了確保安全迭代線程每次next都要先獲取獨佔鎖,得不到便需等待,等到被喚醒繼續執行就需要對數組此時的狀況進行判斷,判斷當前迭代器要獲取的數據是否已經過時,將最新的 takeIndex 賦給迭代器的 cursor,從而確保迭代器不會返回過時的數據。

        private void incorporateDequeues() {
            // assert lock.getHoldCount() == 1;
            // assert itrs != null;
            // assert !isDetached();
            // assert count > 0;

            final int cycles = itrs.cycles;
            final int takeIndex = ArrayBlockingQueue.this.takeIndex;
            final int prevCycles = this.prevCycles;
            final int prevTakeIndex = this.prevTakeIndex;

            if (cycles != prevCycles || takeIndex != prevTakeIndex) {
                final int len = items.length;
                // how far takeIndex has advanced since the previous
                // operation of this iterator
                //計算此時takeIndex 與 迭代器存儲的prevTakeIndex之間的長度
                //接下來要用它來判斷迭代器接下來讀取的數據是否已過時
                long dequeues = (cycles - prevCycles) * len
                    + (takeIndex - prevTakeIndex);

                //接下來就是檢查各個下標變量lastRet,nextIndex,cursor
                //查看它們指向的數據是否已過時,所謂過時指的是此時 takeIndex 已在其前
                //若過時就將lastRet與nextIndex置爲REMOVED,cursor置爲此時的takeIndex
                if (invalidated(lastRet, prevTakeIndex, dequeues, len))
                    lastRet = REMOVED;
                if (invalidated(nextIndex, prevTakeIndex, dequeues, len))
                    nextIndex = REMOVED;
                if (invalidated(cursor, prevTakeIndex, dequeues, len))
                    cursor = takeIndex;
				
				// 這三個下標變量若都<0,說明該終止此迭代器
				// detach會將preTakeIndex置爲DETACHED,然後調用doSomeSweeping清掃迭代器鏈
				//在isDetached中就是通過preTakeIndex是否小於0來判斷迭代器是否終止
                if (cursor < 0 && nextIndex < 0 && lastRet < 0)
                    detach();
                //迭代器沒有作廢的話,更新prevCycles與prevTakeIndex的值
                //回到next方法從takeIndex處開始繼續往下遍歷
                else {
                    this.prevCycles = cycles;
                    this.prevTakeIndex = takeIndex;
                }
            }
        }

invalidated 返回true 說明 index 失效。失效說明該下標變量在 takeIndex 之前。

        private boolean invalidated(int index, int prevTakeIndex,
                                    long dequeues, int length) {
// 下標變量小於0返回false,表明該下標變量的值不需要進行更改
// 有三個狀態 NONE ,REMOVED,DETACHED 它們皆小於0。
//DETACHED:專門用於preTakeIndex使用,isDetached方法通過其來判斷
//NONE:用於三個下標變量:cursor,nextIndex,lastRet;這三個用於迭代功能的實現。
//它們爲NONE,表明迭代結束可能因爲數組爲空或是遍歷完。
//REMOVED:用於lastRet 與 nextIndex。表面數據過時。
            if (index < 0) 
                return false;
            int distance = index - prevTakeIndex;
            if (distance < 0)
                distance += length;
            return dequeues > distance;
        }

detach

        private void detach() {
            // Switch to detached mode 將迭代器轉換爲 detached模式
            // 下面的這些申明都說明了調用該方法時數組的狀態
            // assert lock.getHoldCount() == 1; 持有鎖
            // 下面四個下標變量的狀態都表明迭代器
            // assert cursor == NONE;
            // assert nextIndex < 0; 
            // assert lastRet < 0 || nextItem == null;
            // assert lastRet < 0 ^ lastItem != null;相當於lastRet < 0 && lastItem == null
            if (prevTakeIndex >= 0) {
                // assert itrs != null;
                prevTakeIndex = DETACHED;
                // try to unlink from itrs (but not too hard)
                itrs.doSomeSweeping(true);
            }
        }

重點在於最後的四個 assert 聲明:

  • cursor == NONE
  • nextIndex < 0;
  • lastRet < 0 || nextItem == null
  • lastRet < 0 ^ lastItem != null 相當於lastRet < 0 && lastItem == null

這便是迭代器的 DETACHED 狀態,之後會將prevTakeIndex = DETACHED 用來標識該迭代器此時處於DETACHED 狀態, isDetached 方法會返回true,從而在清掃方法 doSomeSweeping 中將該節點刪除。

detach 方法代碼中做了兩件事:1,prevTakeIndex = DETACHED; 在 isDetached 中便是通過 prevTakeIndex 值是否小於0來判斷迭代器是否處於 DETACHED 狀態。2,調用 doSomeSweeping 刪除該節點,並清掃迭代鏈,注意這裏傳的是true,也就是以最大探測範圍 16 開始探測迭代鏈。爲什麼傳true?爲了儘可能刪除該節點。

這裏有個問題:detach 裏調用 doSomeSweeping 一定能刪除掉該節點嗎?不一定,該方法在上面分析過,它的清掃是從上一個線程清掃結束的地方開始,探測範圍爲4或16,若在範圍內沒探測到就結束,若探測到就擴大探測範圍,範圍恢復爲16再繼續往下探測。

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