Java中的BlockingQueue隊列

  BlockingQueue位於JDK5新增的concurrent包中,它很好地解決了多線程中,如何高效安全地“傳輸”數據的問題。通過這些高效並且線程安全的隊列類,爲我們快速搭建高質量的多線程程序帶來極大的便利。

  阻塞隊列,顧名思義,它首先它是一個隊列,在數據結構中,隊列是一種線性表。

  我們通過一個共享的隊列,可以使得數據由隊列的一端輸入,從另外一端輸出。常用的隊列主要有以下兩種:
  先進先出(FIFO):先插入的隊列的元素也最先出隊列,類似於排隊的功能。從某種程度上來說這種隊列也體現了一種公平性。
  後進先出(LIFO):後插入隊列的元素最先出隊列,這種隊列優先處理最近發生的事件,相當於棧。

  在多線程環境中,通過隊列可以很容易地實現數據共享,比如經典的“生產者”和“消費者”模型中,通過隊列可以很便利地實現兩者之間的數據共享。

  假設我們有若干生產者線程,另外又有若干個消費者線程。如果生產者線程需要把準備好的數據共享給消費者線程,利用隊列的方式來傳遞數據,就可以很方便地解決他們之間的數據共享問題。但是,如果生產者和消費者在某個時間段內,萬一發生數據處理速度不匹配的情況呢?理想情況下,如果生產者產出數據的速度大於消費者消費的速度,並且當生產出來的數據累積到一定程度的時候,那麼生產者必須暫停等待一下(阻塞生產者線程),以便等待消費者線程把累積的數據處理完畢,反之亦然。

  在JDK5的concurrent包發佈以前,在多線程環境下,程序員只能靠自己去人爲地控制這些實現細節,還要兼顧效率和線程安全,這會給我們的程序帶來不小的複雜度。於是,強大的concurrent包橫空出世了,並且給我們帶來了強大的BlockingQueue。(注:在多線程領域,所謂阻塞,在某些情況下會掛起線程(即阻塞),一旦條件滿足,被掛起的線程又會自動被喚醒)。

  阻塞隊列與我們平常接觸的普通隊列(LinkedList或ArrayList等)的最大不同點,在於阻塞隊列提供了阻塞添加和阻塞刪除的方法。

*阻塞添加     
	所謂阻塞添加,是指當阻塞隊列元素已滿時,隊列會阻塞加入元素的線程,直到隊列元素不滿時才重新喚醒線程執行元素加入操作。
*阻塞刪除    
	 阻塞刪除是指在隊列元素爲空時,刪除隊列元素的線程將被阻塞,直到隊列不爲空時再執行刪除操作(一般都會返回被刪除的元素)。

   作爲BlockingQueue的使用者,我們再也不用關心什麼時候需要阻塞線程,什麼時候需要喚醒線程,因爲BlockingQueue把這一切都爲我們包辦了。

  BlockingQueue的核心方法:

  • 插入方法:

    • add(E e) : 添加成功返回true,失敗拋IllegalStateException異常
    • offer(E e) : 成功返回 true,如果此隊列已滿,則返回 false。
    • put(E e) :將元素插入此隊列的尾部,如果該隊列已滿,則一直阻塞
  • 刪除方法:

    • remove(Object o) :移除指定元素,成功返回true,失敗返回false
    • poll() : 獲取並移除此隊列的頭元素,若隊列爲空,則返回 null
    • take():獲取並移除此隊列頭元素,若沒有元素則一直阻塞。
  • 檢查方法

    • element() :獲取但不移除此隊列的頭元素,沒有元素則拋異常
    • peek() :獲取但不移除此隊列的頭;若隊列爲空,則返回 null。

  常見BlockingQueue:

  ①ArrayBlockingQueue

  ArrayBlockingQueue是一個阻塞式的隊列,繼承自AbstractBlockingQueue,間接的實現了Queue接口和Collection接口。底層以數組的形式保存數據(實際上可看作一個循環數組)。常用的操作包括 add ,offer,put,remove,poll,take,peek。 前三者add offer put 是插入的操作。後面四個方法是取出的操作。  

  可以說,ArrayBlockingQueue 是一個用數組實現的有界阻塞隊列,其內部按先進先出的原則對元素進行排序,其中put方法和take方法爲添加和刪除的阻塞方法。

  需要注意的是,ArrayBlockingQueue內部的阻塞隊列是通過重入鎖ReenterLock和Condition條件隊列實現的,所以ArrayBlockingQueue中的元素存在公平訪問與非公平訪問的區別,對於公平訪問隊列,被阻塞的線程可以按照阻塞的先後順序訪問隊列,即先阻塞的線程先訪問隊列。而非公平隊列,當隊列可用時,阻塞的線程將進入爭奪訪問資源的競爭中,也就是說誰先搶到誰就執行,沒有固定的先後順序。

  ②LinkedBlockingQueue

  LinkedBlockingQueue是底層基於鏈表實現的阻塞隊列,內部維持着一個數據緩衝隊列(該隊列由鏈表構成)。當生產者往隊列中放入一個數據時,隊列會從生產者手中獲取數據,並緩存在隊列內部,而生產者立即返回;只有當隊列緩衝區達到最大值緩存容量時(LinkedBlockingQueue可以通過構造函數指定該值),纔會阻塞生產者隊列,直到消費者從隊列中消費掉一份數據,生產者線程會被喚醒,反之對於消費者這端的處理也基於同樣的原理。結構圖如下:

  LinkedBlockingQueue構造的時候若沒有指定大小,則默認大小爲Integer.MAX_VALUE,當然也可以在構造函數的參數中指定大小。LinkedBlockingQueue不接受null。
  LinkedBlockingQueue之所以能夠高效的處理併發數據,還因爲其對於生產者端和消費者端分別採用了獨立的鎖來控制數據同步,這也意味着在高併發的情況下生產者和消費者可以並行地操作隊列中的數據,以此來提高整個隊列的併發性能。

  LinkedBlockingQueue中維持兩把鎖,一把鎖用於入隊,一把鎖用於出隊,這也就意味着,同一時刻,只能有一個線程執行入隊,其餘執行入隊的線程將會被阻塞;同時,可以有另一個線程執行出隊,其餘執行出隊的線程將會被阻塞。換句話說,雖然入隊和出隊兩個操作同時均只能有一個線程操作,但是可以一個入隊線程和一個出隊線程共同執行,也就意味着可能同時有兩個線程在操作隊列,那麼爲了維持線程安全,LinkedBlockingQueue使用一個AtomicInterger類型的變量表示當前隊列中含有的元素個數,所以可以確保兩個線程之間操作底層隊列是線程安全的。

  LinkedBlockingQueue可以指定容量,內部維持一個隊列,所以有一個頭節點head和一個尾節點last,內部維持兩把鎖,一個用於入隊,一個用於出隊,還有鎖關聯的Condition對象。重要字段有:

    //容量,如果沒有指定,該值爲Integer.MAX_VALUE;
    private final int capacity;
    //當前隊列中的元素
    private final AtomicInteger count = new AtomicInteger();
    //隊列頭節點,始終滿足head.item==null
    transient Node<E> head;
    //隊列的尾節點,始終滿足last.next==null
    private transient Node<E> last;
    //用於出隊的鎖
    private final ReentrantLock takeLock = new ReentrantLock();
    //當隊列爲空時,保存執行出隊的線程
    private final Condition notEmpty = takeLock.newCondition();
    //用於入隊的鎖
    private final ReentrantLock putLock = new ReentrantLock();
    //當隊列滿時,保存執行入隊的線程
    private final Condition notFull = putLock.newCondition();

   LinkedBlockingQueue的構造方法有三個:

public LinkedBlockingQueue() {
        this(Integer.MAX_VALUE);
    }

    public LinkedBlockingQueue(int capacity) {
        if (capacity <= 0) throw new IllegalArgumentException();
        this.capacity = capacity;
        last = head = new Node<E>(null);//last和head在隊列爲空時都存在,所以隊列中至少有一個節點
    }

    public LinkedBlockingQueue(Collection<? extends E> c) {
        this(Integer.MAX_VALUE);
        final ReentrantLock putLock = this.putLock;
        putLock.lock(); // Never contended, but necessary for visibility
        try {
            int n = 0;
            for (E e : c) {
                if (e == null)
                    throw new NullPointerException();
                if (n == capacity)
                    throw new IllegalStateException("Queue full");
                enqueue(new Node<E>(e));
                ++n;
            }
            count.set(n);
        } finally {
            putLock.unlock();
        }
    }

   從LinkedBlockingQueue的構造方法中可以看出:當調用無參的構造方法時,容量是int的最大值;隊列中至少包含一個節點,哪怕隊列對外表現爲空;LinkedBlockingQueue不支持null元素。

  ArrayBlockingQueue和LinkedBlockingQueue是兩個最普通、最常用的阻塞隊列。
  LinkedBlockingQueue用一個鏈表保存元素,其內部有一個Node的內部類,其中有一個成員變量 Node next,這樣就形成了一個鏈表的結構,要獲取下一個元素,只要調用next就可以了。而ArrayBlockingQueue則基於數組來保存元素。
  LinkedBlockingQueue內部讀寫(插入獲取)各有一個鎖,而ArrayBlockingQueue則讀寫共享一個鎖。

  ③SynchronousQueue

  不像ArrayBlockingQueue或LinkedBlockingQueue,SynchronousQueue內部並沒有數據緩存空間,你不能調用peek()方法來看隊列中是否有數據元素,因爲數據元素只有當你試着取走的時候纔可能存在,不取走而只想偷窺一下是不行的,當然遍歷這個隊列的操作也是不允許的。隊列頭元素是第一個排隊要插入數據的線程,而不是要交換的數據。數據是在配對的生產者和消費者線程之間直接傳遞的,並不會將數據緩衝到隊列中。可以這樣來理解:生產者和消費者互相等待對方,握手,然後一起離開

  SynchronousQueue的一個使用場景是在線程池裏。Executors.newCachedThreadPool()就使用了SynchronousQueue,這個線程池根據需要(新任務到來時)創建新的線程,如果有空閒線程則會重複使用,線程空閒了60秒後會被回收。 

  ④LinkedBlockingDeque

  LinkedBlockingDeque是一個基於鏈表的雙端阻塞隊列。和LinkedBlockingQueue類似,區別在於該類實現了Deque接口,而LinkedBlockingQueue實現了Queue接口。

  LinkedBlockingDeque是一個可選容量的阻塞隊列,如果沒有設置容量,那麼容量將是Int的最大值。

  LinkedBlockingDeque的底層數據結構是一個雙端隊列,該隊列使用鏈表實現,如圖所示:

 

  LinkedBlockingDeque的重要字段有如下幾個:

    //隊列的頭節點
    transient Node<E> first;
    //隊列的尾節點
    transient Node<E> last;
    //隊列中元素的個數
    private transient int count;
    //隊列中元素的最大個數
    private final int capacity;
    //鎖
    final ReentrantLock lock = new ReentrantLock();
    //隊列爲空時,阻塞take線程的條件隊列
    private final Condition notEmpty = lock.newCondition();
    //隊列滿時,阻塞put線程的條件隊列
    private final Condition notFull = lock.newCondition();

   從上面的字段,可以看到LinkedBlockingDeque內部只有一把鎖以及該鎖上關聯的兩個條件,所以可以推斷同一時刻只有一個線程可以在隊頭或者隊尾執行入隊或出隊操作。可以發現這點和LinkedBlockingQueue不同,LinkedBlockingQueue可以同時有兩個線程在兩端執行操作。

  由於LinkedBlockingDeque是一個雙端隊列,所以就可以在隊頭執行入隊和出隊操作,也可以在隊尾執行入隊和出隊操作。

  LinkedBlockingDeque的構造方法有三個:

public LinkedBlockingDeque() {
        this(Integer.MAX_VALUE);
    }

    public LinkedBlockingDeque(int capacity) {
        if (capacity <= 0) throw new IllegalArgumentException();
        this.capacity = capacity;
    }

    public LinkedBlockingDeque(Collection<? extends E> c) {
        this(Integer.MAX_VALUE);
        final ReentrantLock lock = this.lock;
        lock.lock(); // Never contended, but necessary for visibility
        try {
            for (E e : c) {
                if (e == null)
                    throw new NullPointerException();
                if (!linkLast(new Node<E>(e)))
                    throw new IllegalStateException("Deque full");
            }
        } finally {
            lock.unlock();
        }
    }

   可以看到這三個構造方法的結構和LinkedBlockingQueue是相同的。 但是LinkedBlockingQueue是存在一個哨兵節點維持頭節點的,而LinkedBlockingDeque中是沒有的。  

LinkedBlockingDeque和LinkedBlockingQueue的相同點在於:
	①基於鏈表
	②容量可選,不設置的話,就是Int的最大值
LinkedBlockingDeque和LinkedBlockingQueue的不同點在於:
	①雙端鏈表和單鏈表
	②不存在哨兵節點
	③一把鎖+兩個條件

LinkedBlockingDeque和ArrayBlockingQueue的相同點在於:使用一把鎖+兩個條件維持隊列的同步。

ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
for (int i = 0; i < 10; i++) {
final int index = i;
singleThreadExecutor.execute(new Runnable() {
 
@Override
public void run() {
try {
System.out.println(index);
Thread.sleep(2000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
});
}

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