阻塞隊列

1.阻塞隊列定義
阻塞隊列常用于生產者和消費者的場景,生產者是往隊列里添加元素的線程,消費者是從隊列里拿元素的線程。阻塞隊列就是生產者存放元素的容器,而消費者也只從容器里拿元素。

2.阻塞隊列的兩種阻塞情景
1)當隊列中沒有數據的情況下,消費者端的所有線程都會被自動阻塞(掛起),直到有數據放入隊列。


1417629-02089dea3758a506.jpg

2)當隊列中填滿數據的情況下,生產者端的所有線程都會被自動阻塞(掛起),直到隊列中有空的位置,線程被自動喚醒。


1417629-0e39332e2bfb2665.jpg

3.BlockingQueue的核心方法
1)放入數據
offer(anObject):表示如果可能的話,將anObject加到BlockingQueue里,即如果BlockingQueue可以容納,則返回true,否則返回false。(本方法不阻塞當前執行方法的線程)

put(anObject):把anObject加到BlockingQueue里,如果BlockQueue沒有空間,則調用此方法的線程被阻斷直到BlockingQueue里面有空間再繼續。

offer(E o, long timeout, TimeUnit unit):可以設定等待的時間,如果在指定的時間內,還不能往隊列中加入BlockingQueue,則返回失敗。

2)獲取數據
poll(time):取走BlockingQueue里排在首位的對象,若不能立即取出,則可以等time參數規定的時間,取不到時返回null;

poll(long timeout, TimeUnit unit):從BlockingQueue取出一個隊首的對象,如果在指定時間內,隊列一旦有數據可取,則立即返回隊首的數據。否則直到時間超時還沒有數據可取,返回失敗。

take():取走BlockingQueue里排在首位的對象,若BlockingQueue為空,阻斷進入等待狀態直到BlockingQueue有新的數據被加入;

drainTo():一次性從BlockingQueue獲取所有可用的數據對象(還可以指定獲取數據的個數),通過該方法,可以提升獲取數據效率;不需要多次分批加鎖或釋放鎖。

4.java中的阻塞隊列
JDK7提供了7個阻塞隊列,分別是:
ArrayBlockingQueue :由數組結構組成的有界阻塞隊列。
LinkedBlockingQueue :由鏈表結構組成的有界阻塞隊列。
PriorityBlockingQueue :支持優先級排序的無界阻塞隊列。
DelayQueue:使用優先級隊列實現的無界阻塞隊列。
SynchronousQueue:不存儲元素的阻塞隊列。
LinkedTransferQueue:由鏈表結構組成的無界阻塞隊列。
LinkedBlockingDeque:由鏈表結構組成的雙向阻塞隊列。

下面對這7種隊列分別介紹:
1) ArrayBlockingQueue
用數組實現的有界阻塞隊列。此隊列按照先進先出(FIFO)的原則對元素進行排序。默認情況下不保證訪問者公平的訪問隊列,所謂公平訪問隊列是指阻塞的所有生產者線程或消費者線程,當隊列可用時,可以按照阻塞的先后順序訪問隊列,即先阻塞的生產者線程,可以先往隊列里插入元素,先阻塞的消費者線程,可以先從隊列里獲取元素。通常情況下為了保證公平性會降低吞吐量。我們可以使用以下代碼創建一個公平的阻塞隊列:

ArrayBlockingQueue fairQueue = new  ArrayBlockingQueue(1000,true);
  1. LinkedBlockingQueue
    基于鏈表的阻塞隊列,同ArrayListBlockingQueue類似,此隊列按照先進先出(FIFO)的原則對元素進行排序,其內部也維持著一個數據緩沖隊列(該隊列由一個鏈表構成),當生產者往隊列中放入一個數據時,隊列會從生產者手中獲取數據,并緩存在隊列內部,而生產者立即返回;只有當隊列緩沖區達到最大值緩存容量時(LinkedBlockingQueue可以通過構造函數指定該值),才會阻塞生產者隊列,直到消費者從隊列中消費掉一份數據,生產者線程會被喚醒,反之對于消費者這端的處理也基于同樣的原理。而LinkedBlockingQueue之所以能夠高效的處理并發數據,還因為其對于生產者端和消費者端分別采用了獨立的鎖來控制數據同步,這也意味著在高并發的情況下生產者和消費者可以并行地操作隊列中的數據,以此來提高整個隊列的并發性能。
    作為開發者,我們需要注意的是,如果構造一個LinkedBlockingQueue對象,而沒有指定其容量大小,LinkedBlockingQueue會默認一個類似無限大小的容量(Integer.MAX_VALUE),這樣的話,如果生產者的速度一旦大于消費者的速度,也許還沒有等到隊列滿阻塞產生,系統內存就有可能已被消耗殆盡了。
    ArrayBlockingQueue和LinkedBlockingQueue是兩個最普通也是最常用的阻塞隊列,一般情況下,在處理多線程間的生產者消費者問題,使用這兩個類足以。

  2. PriorityBlockingQueue
    是一個支持優先級的無界隊列。默認情況下元素采取自然順序升序排列。可以自定義實現compareTo()方法來指定元素進行排序規則,或者初始化PriorityBlockingQueue時,指定構造參數Comparator來對元素進行排序。需要注意的是不能保證同優先級元素的順序。

  3. DelayQueue
    是一個支持延時獲取元素的無界阻塞隊列。隊列使用PriorityQueue來實現。隊列中的元素必須實現Delayed接口,在創建元素時可以指定多久才能從隊列中獲取當前元素。只有在延遲期滿時才能從隊列中提取元素。我們可以將DelayQueue運用在以下應用場景:
    緩存系統的設計:可以用DelayQueue保存緩存元素的有效期,使用一個線程循環查詢DelayQueue,一旦能從DelayQueue中獲取元素時,表示緩存有效期到了。

定時任務調度:使用DelayQueue保存當天將會執行的任務和執行時間,一旦從DelayQueue中獲取到任務就開始執行,從比如TimerQueue就是使用DelayQueue實現的。

  1. SynchronousQueue
    是一個不存儲元素的阻塞隊列。每一個put操作必須等待一個take操作,否則不能繼續添加元素。SynchronousQueue可以看成是一個傳球手,負責把生產者線程處理的數據直接傳遞給消費者線程。隊列本身并不存儲任何元素,非常適合于傳遞性場景,比如在一個線程中使用的數據,傳遞給另外一個線程使用,SynchronousQueue的吞吐量高于LinkedBlockingQueue 和 ArrayBlockingQueue。

  2. LinkedTransferQueue
    是一個由鏈表結構組成的無界阻塞TransferQueue隊列。相對于其他阻塞隊列,LinkedTransferQueue多了tryTransfer和transfer方法。
    transfer方法:如果當前有消費者正在等待接收元素(消費者使用take()方法或帶時間限制的poll()方法時),transfer方法可以把生產者傳入的元素立刻transfer(傳輸)給消費者。如果沒有消費者在等待接收元素,transfer方法會將元素存放在隊列的tail節點,并等到該元素被消費者消費了才返回。transfer方法的關鍵代碼如下:

Node pred = tryAppend(s, haveData);
return awaitMatch(s, pred, e, (how == TIMED), nanos);

第一行代碼是試圖把存放當前元素的s節點作為tail節點。第二行代碼是讓CPU自旋等待消費者消費元素。因為自旋會消耗CPU,所以自旋一定的次數后使用Thread.yield()方法來暫停當前正在執行的線程,并執行其他線程。

tryTransfer方法。則是用來試探下生產者傳入的元素是否能直接傳給消費者。如果沒有消費者等待接收元素,則返回false。和transfer方法的區別是tryTransfer方法無論消費者是否接收,方法立即返回。而transfer方法是必須等到消費者消費了才返回。

對于帶有時間限制的tryTransfer(E e, long timeout, TimeUnit unit)方法,則是試圖把生產者傳入的元素直接傳給消費者,但是如果沒有消費者消費該元素則等待指定的時間再返回,如果超時還沒消費元素,則返回false,如果在超時時間內消費了元素,則返回true。

  1. LinkedBlockingDeque
    是一個由鏈表結構組成的雙向阻塞隊列。所謂雙向隊列指的你可以從隊列的兩端插入和移出元素。雙端隊列因為多了一個操作隊列的入口,在多線程同時入隊時,也就減少了一半的競爭。相比其他的阻塞隊列,LinkedBlockingDeque多了addFirst,addLast,offerFirst,offerLast,peekFirst,peekLast等方法,以First單詞結尾的方法,表示插入,獲取(peek)或移除雙端隊列的第一個元素。以Last單詞結尾的方法,表示插入,獲取或移除雙端隊列的最后一個元素。另外插入方法add等同于addLast,移除方法remove等效于removeFirst。但是take方法卻等同于takeFirst,不知道是不是Jdk的bug,使用時還是用帶有First和Last后綴的方法更清楚。

在初始化LinkedBlockingDeque時可以設置容量防止其過渡膨脹。另外雙向阻塞隊列可以運用在“工作竊取”模式中。

5.阻塞隊列實現原理

public class ArrayBlockingQueue<E> extends AbstractQueue<E>
       implements BlockingQueue<E>, java.io.Serializable {

   private static final long serialVersionUID = -817911632652898426L;
   /** The queued items */
   final Object[] items;
   /** items index for next take, poll, peek or remove */
   int takeIndex;
   /** items index for next put, offer, or add */
   int putIndex;
   /** Number of elements in the queue */
   int count;
   final ReentrantLock lock;
   /** Condition for waiting takes */
   private final Condition notEmpty;
   /** Condition for waiting puts */
   private final Condition notFull;
}

從上面代碼可以看出ArrayBlockingQueue是維護一個Object類型的數組,takeIndex和putIndex分別表示隊首元素和隊尾元素的下標,count表示隊列中元素的個數,lock則是一個可重入鎖,notEmpty和notFull是等待條件。接下來我們看看關鍵方法put:

public void put(E e) throws InterruptedException {
        if (e == null) throw new NullPointerException();
        final ReentrantLock lock = this.lock;
        lock.lockInterruptibly();
        try {
            while (count == items.length)
                notFull.await();
            enqueue(e);
        } finally {
            lock.unlock();
        }
    }

從put方法的實現可以看出,它先獲取了鎖,并且獲取的是可中斷鎖,然后判斷當前元素個數是否等于數組的長度,如果相等,則調用notFull.await()進行等待,當被其他線程喚醒時,通過enqueue(e)方法插入元素,最后解鎖。

/**
   * Inserts element at current put position, advances, and signals.
   * Call only when holding lock.
   */
  private void enqueue(E x) {
      // assert lock.getHoldCount() == 1;
      // assert items[putIndex] == null;
      final Object[] items = this.items;
      items[putIndex] = x;
      if (++putIndex == items.length) putIndex = 0;
      count++;
      notEmpty.signal();
  }

插入成功后,通過notEmpty喚醒正在等待取元素的線程。再來看看take方法:

public E take() throws InterruptedException {
       final ReentrantLock lock = this.lock;
       lock.lockInterruptibly();
       try {
           while (count == 0)
               notEmpty.await();
           return dequeue();
       } finally {
           lock.unlock();
       }
   }

跟put方法實現類似,put方法等待的是notFull信號,而take方法等待的是notEmpty信號。在take方法中,如果可以取元素,則通過dequeue方法取得元素,下面是dequeue方法的實現:

private E dequeue() {
      // assert lock.getHoldCount() == 1;
      // assert items[takeIndex] != null;
      final Object[] items = this.items;
      @SuppressWarnings("unchecked")
      E x = (E) items[takeIndex];
      items[takeIndex] = null;
      if (++takeIndex == items.length) takeIndex = 0;
      count--;
      if (itrs != null)
          itrs.elementDequeued();
      notFull.signal();
      return x;
  }

6.阻塞隊列的使用場景
除了線程池的實現使用阻塞隊列之外,我們可以在生產者-消費者模式來使用阻塞隊列,首先使用Object.wait()、Object.notify()和非阻塞隊列實現生產者-消費者模式:

public class Test {
  private int queueSize = 10;
  private PriorityQueue<Integer> queue = new PriorityQueue<Integer>(queueSize);  
  public static void main(String[] args)  {
      Test test = new Test();
      Producer producer = test.new Producer();
      Consumer consumer = test.new Consumer();       
      producer.start();
      consumer.start();
  }

  class Consumer extends Thread{         
      @Override
      public void run() {
          while(true){
              synchronized (queue) {
                  while(queue.size() == 0){
                      try {
                          System.out.println("隊列空,等待數據");
                          queue.wait();
                      } catch (InterruptedException e) {
                          e.printStackTrace();
                          queue.notify();
                      }
                  }
                  queue.poll();          //每次移走隊首元素
                  queue.notify();
              }
          }
      }
  }

  class Producer extends Thread{       
      @Override
      public void run() {
          while(true){
              synchronized (queue) {
                  while(queue.size() == queueSize){
                      try {
                          System.out.println("隊列滿,等待有空余空間");
                          queue.wait();
                      } catch (InterruptedException e) {
                          e.printStackTrace();
                          queue.notify();
                      }
                  }
                  queue.offer(1);        //每次插入一個元素
                  queue.notify();
              }
          }
      }
  }       
}

下面是使用阻塞隊列實現的生產者-消費者模式:

public class Test {
   private int queueSize = 10;
   private ArrayBlockingQueue<Integer> queue = new ArrayBlockingQueue<Integer>(queueSize); 
   public static void main(String[] args)  {
       Test test = new Test();
       Producer producer = test.new Producer();
       Consumer consumer = test.new Consumer();         
       producer.start();
       consumer.start();
   }

   class Consumer extends Thread{  
       @Override
       public void run() {
           while(true){
               try {
                   queue.take();
               } catch (InterruptedException e) {
                   e.printStackTrace();
               }
           }
       }   
   }

   class Producer extends Thread{    
       @Override
       public void run() {         
           while(true){
               try {
                   queue.put(1);
               } catch (InterruptedException e) {
                   e.printStackTrace();
               }
           }
       }     
   }
}

參考文章:http://www.lxweimin.com/p/057e94b71df9

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,517評論 6 539
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,087評論 3 423
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,521評論 0 382
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,493評論 1 316
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,207評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,603評論 1 325
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,624評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,813評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,364評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,110評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,305評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,874評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,532評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,953評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,209評論 1 291
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,033評論 3 396
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,268評論 2 375

推薦閱讀更多精彩內容