【java多線程】(8)---阻塞隊列

來源:互聯網
上載者:User

標籤:java多線程   加鎖   作用   隊列實現   time   平衡   sig   tps   建立   

阻塞隊列

      再寫阻塞列隊之前,我寫了一篇有關queue集合相關部落格,也主要是為這篇做鋪墊的。

      網址:【java提高】---queue集合  在這篇部落格中我們接觸的隊列都是非阻塞隊列,比如PriorityQueue、LinkedList(LinkedList是雙向鏈表,它實現了Dequeue介面)。

      使用非阻塞隊列的時候有一個很大問題就是:它不會對當前線程產生阻塞,那麼在面對類似消費者-生產者的模型時,就必須額外地實現同步策略以及線程間喚醒策略,這個實現起來就非常麻煩。

一、認識BlockingQueue

       阻塞隊列,顧名思義,首先它是一個隊列,而一個隊列在資料結構中所起的作用大致如所示:

      從我們可以很清楚看到,通過一個共用的隊列,可以使得資料由隊列的一端輸入,從另外一端輸出;

常用的隊列主要有以下兩種:

  先進先出(FIFO):先插入的隊列的元素也最先出隊列,類似於排隊的功能。從某種程度上來說這種隊列也體現了一種公平性。

  後進先出(LIFO):後插入隊列的元素最先出隊列,這種隊列優先處理最近發生的事件。

      阻塞隊列常用於生產者和消費者的情境,生產者線程可以把生產結果存到阻塞隊列中,而消費者線程把中間結果取出並在將來修改它們。

隊列會自動平衡負載,如果生產者線程集啟動並執行比消費者線程集慢,則消費者線程集在等待結果時就會阻塞;如果生產者線程集啟動並執行快,那麼它將等待消費者線程集趕上來。

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

看下BlockingQueue的核心方法

1、放入資料

  (1)put(E e):put方法用來向隊尾存入元素,如果隊列滿,則等待。    

  (2)offer(E o, long timeout, TimeUnit unit):offer方法用來向隊尾存入元素,如果隊列滿,則等待一定的時間,當時間期限達到時,如果還沒有插入成功,則返回false;否則返回true;

2、擷取資料

 (1)take():take方法用來從隊首取元素,如果隊列為空白,則等待;

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

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

 (4)poll(long timeout, TimeUnit unit):poll方法用來從隊首取元素,如果隊列空,則等待一定的時間,當時間期限達到時,如果取到,則返回null;否則返回取得的元素;

 

二、常見BlockingQueue

       在瞭解了BlockingQueue的準系統後,讓我們來看看BlockingQueue家庭大致有哪些成員?

1、ArrayBlockingQueue

      基於數組實現的一個阻塞隊列,在建立ArrayBlockingQueue對象時必須制定容量大小。並且可以指定公平性與非公平性,預設情況下為非公平的,即不保證等待時間最長的隊列最優先能夠訪問隊列。

2、LinkedBlockingQueue

     基於鏈表實現的一個阻塞隊列,在建立LinkedBlockingQueue對象時如果不指定容量大小,則預設大小為Integer.MAX_VALUE。

3、PriorityBlockingQueue

       以上2種隊列都是先進先出隊列,而PriorityBlockingQueue卻不是,它會按照元素的優先順序對元素進行排序,按照優先順序順序出隊,每次出隊的元素都是優先順序最高的元素。注意,此阻塞隊列為無界阻塞隊列,即

容量沒有上限(通過源碼就可以知道,它沒有容器滿的訊號標誌),前面2種都是有界隊列。

4、DelayQueue

       基於PriorityQueue,一種延時阻塞隊列,DelayQueue中的元素只有當其指定的延遲時間到了,才能夠從隊列中擷取到該元素。DelayQueue也是一個無界隊列,因此往隊列中插入資料的操作(生產者)永遠不會

被阻塞,而只有擷取資料的操作(消費者)才會被阻塞。

 5、小案例

       有關生產者-消費者,上篇部落格我寫了基於wait和notifyAll實現過,也基於await和signal實現過,網址:https://www.cnblogs.com/qdhxhz/p/9206076.html

這裡已經是第三個相關生產消費者的小案例了。

      這裡通過LinkedBlockingQueue實現生產消費模式

(1)測試類別

public class BlockingQueueTest {                public static void main(String[] args) throws InterruptedException {              // 聲明一個容量為10的緩衝隊列             BlockingQueue<String> queue = new LinkedBlockingQueue<String>(10);                   //new了兩個生產者和一個消費者,同時他們共用一個queue緩衝隊列             Producer producer1 = new Producer(queue);             Producer producer2 = new Producer(queue);                       Consumer consumer = new Consumer(queue);                   // 通過線程池啟動線程             ExecutorService service = Executors.newCachedThreadPool();             service.execute(producer1);             service.execute(producer2);                       service.execute(consumer);                   // 執行5s             Thread.sleep(5 * 1000);             producer1.stop();             producer2.stop();                        Thread.sleep(2000);             // 退出Executor             service.shutdown();         }     }

(2)生產者

/**  * 生產者線程  */ public class Producer implements Runnable {          private volatile boolean  isRunning = true;//是否在運行標誌     private BlockingQueue<String> queue;//阻塞隊列     private static AtomicInteger count = new AtomicInteger();//自動更新的值         //建構函式     public Producer(BlockingQueue<String> queue) {         this.queue = queue;     }       public void run() {         String data = null;         System.out.println(Thread.currentThread().getName()+" 啟動生產者線程!");         try {             while (isRunning) {                 Thread.sleep(1000);                                 //以原子方式將count當前值加1                 data = "" + count.incrementAndGet();                 System.out.println(Thread.currentThread().getName()+" 將生產資料:" + data + "放入隊列中");                                //設定的等待時間為2s,如果超過2s還沒加進去返回false                 if (!queue.offer(data, 2, TimeUnit.SECONDS)) {                     System.out.println(Thread.currentThread().getName()+" 放入資料失敗:" + data);                 }             }         } catch (InterruptedException e) {             e.printStackTrace();             Thread.currentThread().interrupt();         } finally {             System.out.println(Thread.currentThread().getName()+" 退出生產者線程!");         }     }       public void stop() {         isRunning = false;     } }

(3)消費者

/**  * 消費者線程  */ public class Consumer implements Runnable {          private BlockingQueue<String> queue;     //建構函式     public Consumer(BlockingQueue<String> queue) {         this.queue = queue;     }       public void run() {         System.out.println(Thread.currentThread().getName()+" 啟動消費者線程!");         boolean isRunning = true;         try {             while (isRunning) {                //有資料時直接從隊列的隊首取走,無資料時阻塞,在2s內有資料,取走,超過2s還沒資料,返回失敗                 String data = queue.poll(2, TimeUnit.SECONDS);                                  if (null != data) {                     System.out.println(Thread.currentThread().getName()+" 正在消費資料:" + data);                     Thread.sleep(1000);                 } else {                     // 超過2s還沒資料,認為所有生產線程都已經退出,自動結束消費線程。                     isRunning = false;                 }             }         } catch (InterruptedException e) {             e.printStackTrace();             Thread.currentThread().interrupt();         } finally {             System.out.println(Thread.currentThread().getName()+" 退出消費者線程!");         }     }      }

運行結果(其中一種)

 

 三、阻塞隊列的實現原理

     主要看兩個關鍵方法的實現:put()和take()

 1、put方法
public void put(E e) throws InterruptedException {        //首先可以看出,不能放null,否在報null 指標異常    if (e == null) throw new NullPointerException();    final E[] items = this.items;        //發現採用的是Lock鎖    final ReentrantLock lock = this.lock;        //如果當前線程不能擷取鎖則拋出異常    lock.lockInterruptibly();    try {        try {            while (count == items.length)    //這裡才是關鍵,我們發現它的堵塞其實是通過await()和signal()來實現的                notFull.await();        } catch (InterruptedException ie) {            notFull.signal();             throw ie;        }        insert(e);    } finally {        lock.unlock();    }}

       當被其他線程喚醒時,通過insert(e)方法插入元素,最後解鎖。

我們看一下insert方法的實現:

private void insert(E x) {    items[putIndex] = x;    putIndex = inc(putIndex);    ++count;    notEmpty.signal();}

      它是一個private方法,插入成功後,通過notEmpty喚醒正在等待取元素的線程。

 2、take()方法
public E take() throws InterruptedException {    final ReentrantLock lock = this.lock;    lock.lockInterruptibly();    try {        try {            while (count == 0)                notEmpty.await();        } catch (InterruptedException ie) {            notEmpty.signal();             throw ie;        }        E x = extract();        return x;    } finally {        lock.unlock();    }}

        跟put方法實現很類似,只不過put方法等待的是notFull訊號,而take方法等待的是notEmpty訊號。在take方法中,如果可以取元素,則通過extract方法取得元素,

下面是extract方法的實現:

private E extract() {    final E[] items = this.items;    E x = items[takeIndex];    items[takeIndex] = null;    takeIndex = inc(takeIndex);    --count;    notFull.signal();    return x;}

跟insert方法也很類似。

其實從這裡大家應該明白了阻塞隊列的實現原理,事實它和我們用Object.wait()、Object.notify()和非阻塞隊列實現生產者-消費者的思路類似,只不過它這裡通過await()和signal()一起整合到了阻塞隊列中實現。

 

 

 想太多,做太少,中間的落差就是煩惱。想沒有煩惱,要麼別想,要麼多做。少校【15】   

 

【java多線程】(8)---阻塞隊列

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.