Java 多線程之 TransferQueue

最近在閱讀開源項目裏,發現有幾個工程都不盡相同地使用LinkedTransferQueue這個數據結構。比如netty,grizzly,xmemcache,Bonecp。Bonecp還擴展出一個BoundTransferQueue。
LinkedTransferQueue最早出現在JSR66R(一個輕量級並行執行框架)包中,目前已合併到JDK7中。JSR66的負責人正是大名頂頂的Doug Lea.
雖然LinkedTransferQueue被集成在JDK7中,但目前主流的JDK平臺仍然是JDK6。以致開源項目開發者都不迫及地把他集成在自已的項目中。
Doug Lea說LinkedTransferQueue是一個聰明的隊列,他是ConcurrentLinkedQueue, 
SynchronousQueue (in “fair” mode), and unbounded LinkedBlockingQueue的超集。

有一篇論文討論了其算法與性能:地址:http://www.cs.rice.edu/~wns1/papers/2006-PPoPP-SQ.pdf

LinkedTransferQueue實現了一個重要的接口TransferQueue,該接口含有下面幾個重要方法:
1. transfer(E e)
   若當前存在一個正在等待獲取的消費者線程,即立刻移交之;否則,會插入當前元素e到隊列尾部,並且等待進入阻塞狀態,到有消費者線程取走該元素。
2. tryTransfer(E e)
   若當前存在一個正在等待獲取的消費者線程(使用take()或者poll()函數),使用該方法會即刻轉移/傳輸對象元素e;
   若不存在,則返回false,並且不進入隊列。這是一個不阻塞的操作。
3. tryTransfer(E e, long timeout, TimeUnit unit)
   若當前存在一個正在等待獲取的消費者線程,會立即傳輸給它; 否則將插入元素e到隊列尾部,並且等待被消費者線程獲取消費掉,
   若在指定的時間內元素e無法被消費者線程獲取,則返回false,同時該元素被移除。
4. hasWaitingConsumer()
   判斷是否存在消費者線程
5. getWaitingConsumerCount()
   獲取所有等待獲取元素的消費線程數量

其實transfer方法在SynchronousQueue的實現中就已存在了,只是沒有做爲API暴露出來。SynchronousQueue有一個特性:它本身不存在容量,只能進行線程之間的
元素傳送。SynchronousQueue在執行offer操作時,如果沒有其他線程執行poll,則直接返回false.線程之間元素傳送正是通過transfer方法完成的。

有一個使用案例,我們知道ThreadPoolExecutor調節線程的原則是:先調整到最小線程,最小線程用完後,他會將優先將任務放入緩存隊列(offer(task)),等緩衝隊列用完了,纔會向最大線程數調節。這似乎與我們所理解的線程池模型有點不同。我們一般採用增加到最大線程後,纔會放入緩衝隊列中,以達到最大性能。ThreadPoolExecutor代碼段:

  public void execute(Runnable command) {
        if (command == null)
            throw new NullPointerException();
        if (poolSize >= corePoolSize || !addIfUnderCorePoolSize(command)) {
            if (runState == RUNNING && workQueue.offer(command)) {
                if (runState != RUNNING || poolSize == 0)
                    ensureQueuedTaskHandled(command);
            }
            else if (!addIfUnderMaximumPoolSize(command))
                reject(command); // is shutdown or saturated
        }
    }
如果我們採用SynchronousQueue作爲ThreadPoolExecuto的緩衝隊列時,在沒有線程執行poll時(即存在等待線程),則workQueue.offer(command)返回false,這時ThreadPoolExecutor就會增加線程,最快地達到最大線程數。但也僅此而已,也因爲SynchronousQueue本身不存在容量,也決定了我們一般無法採用SynchronousQueue作爲ThreadPoolExecutor的緩存隊列。而一般採用LinkedBlockingQueue的offer方法來實現。最新的LinkedTransferQueue也許可以幫我們解決這個問題,後面再說。

transfer算法比較複雜,實現很難看明白。大致的理解是採用所謂雙重數據結構(dual data structures)。之所以叫雙重,其原因是方法都是通過兩個步驟完成:
保留與完成。比如消費者線程從一個隊列中取元素,發現隊列爲空,他就生成一個空元素放入隊列,所謂空元素就是數據項字段爲空。然後消費者線程在這個字段上旅轉等待。這叫保留。直到一個生產者線程意欲向隊例中放入一個元素,這裏他發現最前面的元素的數據項字段爲NULL,他就直接把自已數據填充到這個元素中,即完成了元素的傳送。大體是這個意思,這種方式優美了完成了線程之間的高效協作。

對於LinkedTransferQueue,Doug Lea進行了盡乎極致的優化。Grizzly的採用了PaddedAtomicReference:
   public LinkedTransferQueue() {
        QNode dummy = new QNode(null, false);
        head = new PaddedAtomicReference<QNode>(dummy);
        tail = new PaddedAtomicReference<QNode>(dummy);
        cleanMe = new PaddedAtomicReference<QNode>(null);
    }
   static final class PaddedAtomicReference<T> extends AtomicReference<T> {        // enough padding for 64bytes with 4byte refs
        Object p0, p1, p2, p3, p4, p5, p6, p7, p8, p9, pa, pb, pc, pd, pe;
        PaddedAtomicReference(T r) { super(r); }
    }
PaddedAtomicReference相對於父類AtomicReference只做了一件事情,就將共享變量追加到64字節。我們可以來計算下,一個對象的引用佔4個字節,
它追加了15個變量共佔60個字節,再加上父類的Value變量,一共64個字節。這麼做的原因。請參考http://www.infoq.com/cn/articles/ftf-java-volatile
http://rdc.taobao.com/team/jm/archives/1719 這兩文章。做JAVA,如果想成爲Doug Lea這樣的大師,也要懂體系結構(待續)


地址:http://guojuanjun.blog.51cto.com/277646/948298/

發佈了62 篇原創文章 · 獲贊 138 · 訪問量 30萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章