Java併發6:阻塞佇列,Fork/Join框架

mortal同學發表於2019-01-02

阻塞佇列

阻塞佇列是一個支援兩個附加操作的佇列。這兩個附加的操作支援阻塞的插入和移除方法:

  • 支援阻塞的插入方法:佇列滿時,佇列會阻塞插入元素的執行緒,直到佇列不滿
  • 支援阻塞的移除方法:佇列空時,獲取元素的執行緒會等待佇列變為非空

阻塞佇列常用於生產者消費者的場景。其中生產者是向佇列新增元素的執行緒,消費者是從佇列取出元素的執行緒,阻塞佇列是存放和獲取元素的容器。

阻塞佇列的4種處理方式:

  1. 丟擲異常:
    • add(e) 當佇列滿,再插入元素,丟擲異常
    • remove() 當佇列空,再刪除元素,丟擲異常
    • element() 獲取元素
  2. 返回特殊值:
    • offer(e) 插入元素時,插入成功返回true
    • poll() 移除元素,成功返回該值,否則返回null
    • peek()
  3. 一直阻塞:
    • put(e) 當阻塞佇列滿時,再插入時會阻塞生產者執行緒,直到佇列可用或中斷退出
    • take() 當佇列空,再移除元素會阻塞消費者執行緒,直到佇列不空
  4. 超時退出
    • offer(e, time, unit) 佇列滿時再插入元素,阻塞,超時退出
    • poll(time, unit) 佇列空時移除元素,阻塞,超時退出

Java中幾種阻塞佇列

  • ArrayBlockingQueue: 陣列結構構成的有界 FIFO 阻塞佇列
  • LinkedBolckingQueue: 連結串列結構構成的有界 FIFO 阻塞佇列
  • PriorityBlockingQueue: 支援優先順序排序的無界阻塞佇列
  • DelayQueue: 支援延時獲取元素,使用優先順序佇列實現的無界阻塞佇列
  • SynchronousQueue: 不儲存元素的阻塞佇列,不為佇列元素維護儲存空間
  • LinkedTransferQueue: 連結串列結構構成的無界阻塞佇列
  • LinkedBlockingDeque: 連結串列構成的雙向阻塞佇列

ArrayBlockingQueue

ArrayBlockingQueue 是一個用陣列實現的有界的,按照 FIFO 原則對元素排序的阻塞佇列。它還支援對等待的生產者和消費者執行緒進行排序時的可選公平策略,預設情況下不保證執行緒公平的訪問,在構造時可以選擇公平策略。公平性會降低吞吐量,但是減少了可變性和避免了“不平衡性”。

LinkedBlockingQueue

這是一個用連結串列實現的有界阻塞佇列,預設長度和最大長度都是 Integer.MAX_VALUE 。該佇列也是按照 FIFO 原則對元素排序,確定執行緒執行的先後順序。

PriorityBlockingQueue

這是一個支援優先順序的無界祖蘇佇列,預設情況下采取自然順序升序排序,也可以通過建構函式指定 Comparator 來對元素進行排序。但是它不能保證相同優先順序元素的順序。

底層是採用二叉最大堆來實現優先順序排序的。

DelayQueue

這是一個支援延時獲取元素的無界阻塞佇列,其佇列使用優先佇列 PriorityQueue 實現。佇列中的元素必須實現 Delayed 介面,建立元素時可以指定多久之後才能從佇列中獲取該元素,只有在元素到期時才能獲取。

主要用於快取,如清除緩衝中超時的資料。還用於定時任務的排程。

元素建立時,要實現 Delayed 介面,首先進行初始化;然後實現 getDelay(Timeunit unit)方法,返回的值是當前元素還需要延時多長時間;最後實現compareTo(Delayed other)方法,用來指定元素的順序。

當消費者從佇列中獲取元素時,如果元素還沒有到延時時間,就阻塞當前執行緒。此外,設定了 leader 變數表示等待獲取佇列頭部元素的執行緒。如果 leader 不為空,表示有現成等待獲取佇列頭部元素,使用 await() 方法讓當前執行緒等待訊號。如果 leader 為空,則把當前執行緒設定為 leader,使用 awaitNanos() 方法讓當前執行緒等待接收訊號或等待 delay 時間。

SynchronousQueue

與其他阻塞佇列不同,這是一個不儲存元素的阻塞佇列,每一個 put 操作必須要等待一個take操作,否則不能繼續新增元素,反之亦然。分為公平和不公平訪問佇列,預設情況採用非公平性策略訪問佇列。

該種佇列本身不儲存任何元素,適合傳遞性場景,把生產者執行緒處理的資料直接傳遞給消費者執行緒,其吞吐量高於 LinkedBlockingQueue 和 ArrayBlockingQueue。

LinkedTransferQueue

這是一個由連結串列結構組成的 FIFO 的無界阻塞 TransferQueue 佇列。它採取一種預佔模式,也就是有就直接拿走,沒有就佔著這個位置直到拿到、超時或中斷。相對於其他阻塞佇列,多了 tryTransfer 方法和 transfer 方法。

  • transfer(e,[timeout,unit]) 方法: 如果當前有消費者正等待接收元素,該方法可以把生產者傳入的元素立刻傳輸給消費者。如果沒有消費者等待,該方法將元素存放在佇列的 tail 節點,等到該元素被消費者消費了才返回。
  • tryTransfer(e,[timeout,unit])方法: 試探生產者傳入的元素是否能直接傳給消費者。如果沒有消費者等待接收元素,返回false。該方法無論消費者是否接收都立即返回,而 transfer 方法必須等消費了才返回。

LinkedBlockingDeque

是一個由連結串列組成的雙向阻塞佇列。可以從佇列兩端插入和移除元素。

Fork/Join框架

該框架主要應用在平行計算中,把一個大人物分割成若干個小任務,最終彙總每個小任務結果後得到大結果的框架。Fork 就是把一個大任務切分成若干子任務並行的執行,Join 就是合併這些子任務的執行結果,最終得到這個大任務的結果。

工作竊取演算法

工作竊取是指某個執行緒從其他佇列裡竊取任務來執行。通常使用雙端佇列,被竊取任務執行緒永遠從雙端佇列頭部拿任務執行,竊取任務的執行緒永遠從雙端佇列尾部拿任務執行。

優點是充分利用執行緒進行平行計算,減少了執行緒間的競爭。缺點是在某些情況下存在競爭,比如佇列只有一個任務時,會消耗更多的資源。

框架設計思路

首先,分割任務,將一個大任務分割成子任務,不停分割直到分割出的子任務足夠小。

然後,執行任務併合並結果。分割的子任務分別放在雙端佇列,然後幾個啟動執行緒分別從雙端佇列獲取任務執行。執行結果放在一個佇列裡,啟動一個執行緒從佇列拿資料,然後合併這些執行緒。

示例

public class ForkJoinCase extends RecursiveTask<Integer> {
    private final int threshold=5;
    private int first;
    private int last;

    public ForkJoinCase(int first,int last){
        this.first=first;
        this.last=last;
    }


    @Override
    protected Integer compute() {
        int ret=0;
        if(last-first<=threshold){//任務足夠小,執行
            for(int i=first;i<=last;i++){
                ret+=i;
            }
        }else{//分解任務
            int mid=first+(last-first)/2;
            ForkJoinCase leftTask=new ForkJoinCase(first,mid);
            ForkJoinCase rightTask=new ForkJoinCase(mid+1,last);
            //執行子任務
            leftTask.fork();
            rightTask.fork();
            //合併子任務結果
            ret=leftTask.join()+rightTask.join();
        }
        return ret;
    }
}
複製程式碼

參考資料

相關文章