BlockQueue的使用

BlockingQueue詳解
    阻塞隊列,顧名思義,首先它是一個隊列,
    
    常用的隊列主要有以下兩種:
        先進先出(FIFO):先插入的隊列的元素也最先出隊列,類似於排隊的功能。
            從某種程度上來說這種隊列也體現了一種公平性。
          後進先出(LIFO):後插入隊列的元素最先出隊列,這種隊列優先處理最近發生的事件。

    阻塞隊列的核心就是生產者和消費着
        當隊列中沒有數據的情況下,消費者端的所有線程都會被自動阻塞(掛起),直到有數據放入隊列
        隊列中填滿數據的情況下,生產者端的所有線程都會被自動阻塞(掛起),直到隊列中有空的位置,線程被自動喚醒。

    
    BlockingQueue的核心方法:
        放入數據:    
        offer(anObject):表示如果可能的話,將anObject加到BlockingQueue裏,即如果BlockingQueue可以容納,
            則返回true,否則返回false.(本方法不阻塞當前執行方法的線程)
        offer(E o, long timeout, TimeUnit unit),可以設定等待的時間,如果在指定的時間內,還不能往隊列中
            加入BlockingQueue,則返回失敗。
        put(anObject):把anObject加到BlockingQueue裏,如果BlockQueue沒有空間,則調用此方法的線程被阻斷
            直到BlockingQueue裏面有空間再繼續.

        獲取數據:
          poll(time):取走BlockingQueue裏排在首位的對象,若不能立即取出,則可以等time參數規定的時間,
            取不到時返回null;
          poll(long timeout, TimeUnit unit):從BlockingQueue取出一個隊首的對象,如果在指定時間內,
            隊列一旦有數據可取,則立即返回隊列中的數據。否則知道時間超時還沒有數據可取,返回null。
          take():取走BlockingQueue裏排在首位的對象,若BlockingQueue爲空,阻斷進入等待狀態直到
            BlockingQueue有新的數據被加入;
          drainTo():一次性從BlockingQueue獲取所有可用的數據對象(還可以指定獲取數據的個數),
            通過該方法,可以提升獲取數據效率;不需要多次分批加鎖或釋放鎖。

    BlockingQueue成員詳細介紹
        重點看前兩個    
        
        1. ArrayBlockingQueue
            基於數組的阻塞隊列實現,在ArrayBlockingQueue內部,維護了一個定長數組,
            以便緩存隊列中的數據對象,這是一個常用的阻塞隊列,除了一個定長數組外,
            ArrayBlockingQueue內部還保存着兩個整形變量,分別標識着隊列的頭部和尾部在數組中的位置。

            ArrayBlockingQueue在生產者放入數據和消費者獲取數據,都是共用同一個鎖對象,
            由此也意味着兩者無法真正並行運行,這點尤其不同於LinkedBlockingQueue;
            按照實現原理來分析,ArrayBlockingQueue完全可以採用分離鎖,從而實現生產者和消費者操作的完全並行運行。
            Doug Lea之所以沒這樣去做,也許是因爲ArrayBlockingQueue的數據寫入和獲取操作已經足夠輕巧,
            以至於引入獨立的鎖機制,除了給代碼帶來額外的複雜性外,其在性能上完全佔不到任何便宜。
            ArrayBlockingQueue和LinkedBlockingQueue間還有一個明顯的不同之處在於,
            前者在插入或刪除元素時不會產生或銷燬任何額外的對象實例,而後者則會生成一個額外的Node對象。
            這在長時間內需要高效併發地處理大批量數據的系統中,其對於GC的影響還是存在一定的區別。
            而在創建ArrayBlockingQueue時,我們還可以控制對象的內部鎖是否採用公平鎖,默認採用非公平鎖。

        2. LinkedBlockingQueue
            基於鏈表的阻塞隊列,同ArrayListBlockingQueue類似,其內部也維持着一個數據緩衝隊列(該隊列由一個鏈表構成),
            當生產者往隊列中放入一個數據時,隊列會從生產者手中獲取數據,並緩存在隊列內部,而生產者立即返回;
            只有當隊列緩衝區達到最大值緩存容量時(LinkedBlockingQueue可以通過構造函數指定該值),纔會阻塞生產者隊列,
            直到消費者從隊列中消費掉一份數據,生產者線程會被喚醒,反之對於消費者這端的處理也基於同樣的原理。
            而LinkedBlockingQueue之所以能夠高效的處理併發數據,還因爲其對於生產者端和消費者端分別採用了獨立的鎖來控制數據同步,
            這也意味着在高併發的情況下生產者和消費者可以並行地操作隊列中的數據,以此來提高整個隊列的併發性能。
            
            作爲開發者,我們需要注意的是,如果構造一個LinkedBlockingQueue對象,而沒有指定其容量大小,
            LinkedBlockingQueue會默認一個類似無限大小的容量(Integer.MAX_VALUE),

            這樣的話,如果生產者的速度一旦大於消費者的速度,也許還沒有等到隊列滿阻塞產生,系統內存就有可能已被消耗殆盡了。


例子:生產者與消費者
				package test;

				import java.util.Random;
				import java.util.concurrent.BlockingQueue;
				import java.util.concurrent.TimeUnit;
				import java.util.concurrent.atomic.AtomicInteger;

				public class Producer implements Runnable {

					private volatile boolean isRunning = true;
					private BlockingQueue queue;
					private static AtomicInteger count = new AtomicInteger();
					private static final int DEFAULT_RANGE_FOR_SLEEP = 1000;
					
					public Producer(BlockingQueue queue){
						this.queue = queue;
					}
					
					@Override
					public void run() {
						String data = null;
						Random r = new Random();
						
						System.out.println("====啓動生產者====");
						try{
							while(isRunning){
								System.out.println("====正在生產數據====");
								Thread.sleep(r.nextInt(DEFAULT_RANGE_FOR_SLEEP));
								
								data = "data:" + count.incrementAndGet();
								if(!queue.offer(data, 2, TimeUnit.SECONDS)){
									System.out.println("放入數據失敗:"+data);
									continue;
								}
								
								System.out.println("將數據:" + data + "放入隊列...");
								
							}
						}catch(InterruptedException e){
							e.printStackTrace();
							Thread.currentThread().interrupt();
						}finally{
							System.out.println("====退出生產者線程====");
						}
					}
					
					public void stop(){
						isRunning = false;
					}
					
				}

				package test;

				import java.util.Random;
				import java.util.concurrent.BlockingQueue;
				import java.util.concurrent.TimeUnit;

				public class Consumer implements Runnable{
					
					private BlockingQueue<String> queue;
					private static final int DEFAULT_RANGE_FOR_SLEEP = 1000;
					
					public Consumer(BlockingQueue<String> queue){
						this.queue = queue;
					}

					@Override
					public void run() {
						System.out.println("====啓動消費者線程!====");
						Random r = new Random();
						boolean isRunning = true;
						
						try{
							while(isRunning){
								System.out.println("====從隊列獲取數據====");
								String data = queue.poll(2, TimeUnit.SECONDS);
								if(null != data){
									System.out.println("拿到數據:"+data);
									System.out.println("消費數據:"+data);
									Thread.sleep(r.nextInt(DEFAULT_RANGE_FOR_SLEEP));
								}else{
									isRunning = false;
								}
							}
						}catch(InterruptedException e){
							e.printStackTrace();
							Thread.currentThread().interrupt();
						}finally{
							System.out.println("====退出消費者線程!====");
						}
					}
					
				}

				package test;

				import java.util.concurrent.BlockingQueue;
				import java.util.concurrent.ExecutorService;
				import java.util.concurrent.Executors;
				import java.util.concurrent.LinkedBlockingQueue;


				public class Test {

					public static void main(String[] args) throws InterruptedException {
						BlockingQueue<String> queue = new LinkedBlockingQueue<String>(10);
						
						Producer producer1 = new Producer(queue);
						Producer producer2 = new Producer(queue);
						Producer producer3 = new Producer(queue);
						Consumer consumer = new Consumer(queue);
						
						ExecutorService service = Executors.newCachedThreadPool();
						service.execute(producer1);
					service.execute(producer2);
					service.execute(producer3);
					service.execute(consumer);

					Thread.sleep(10 * 1000); //生產者執行10m
					producer1.stop();
					producer2.stop();
					producer3.stop();
					
					Thread.sleep(2000);
					
					//退出Executor
					service.shutdown();
					}
				}
3. DelayQueue
            DelayQueue中的元素只有當其指定的延遲時間到了,才能夠從隊列中獲取到該元素。
            DelayQueue是一個沒有大小限制的隊列,因此往隊列中插入數據的操作(生產者)永遠不會被阻塞,
            而只有獲取數據的操作(消費者)纔會被阻塞。
            
            使用場景:
              DelayQueue使用場景較少,但都相當巧妙,常見的例子比如使用一個DelayQueue來管理一個超時未響應的連接隊列。

        4. PriorityBlockingQueue
            基於優先級的阻塞隊列(優先級的判斷通過構造函數傳入的Compator對象來決定),
            但需要注意的是PriorityBlockingQueue並不會阻塞數據生產者,
            而只會在沒有可消費的數據時,阻塞數據的消費者。
            因此使用的時候要特別注意,生產者生產數據的速度絕對不能快於消費者消費數據的速度,
            否則時間一長,會最終耗盡所有的可用堆內存空間。在實現PriorityBlockingQueue時,內部控制線程同步的鎖採用的是公平鎖。

        5. SynchronousQueue
            種無緩衝的等待隊列,類似於無中介的直接交易,有點像原始社會中的生產者和消費者,
            生產者拿着產品去集市銷售給產品的最終消費者,而消費者必須親自去集市找到所要商品的直接生產者,
            如果一方沒有找到合適的目標,那麼對不起,大家都在集市等待。相對於有緩衝的BlockingQueue來說,
            少了一箇中間經銷商的環節(緩衝區),如果有經銷商,生產者直接把產品批發給經銷商,
            而無需在意經銷商最終會將這些產品賣給那些消費者,由於經銷商可以庫存一部分商品,因此相對於直接交易模式,
            總體來說採用中間經銷商的模式會吞吐量高一些(可以批量買賣);但另一方面,又因爲經銷商的引入,
            使得產品從生產者到消費者中間增加了額外的交易環節,單個產品的及時響應性能可能會降低。

            聲明一個SynchronousQueue有兩種不同的方式,它們之間有着不太一樣的行爲。公平模式和非公平模式的區別:
            如果採用公平模式:SynchronousQueue會採用公平鎖,並配合一個FIFO隊列來阻塞多餘的生產者和消費者,從而體系整體的公平策略;
            但如果是非公平模式(SynchronousQueue默認):SynchronousQueue採用非公平鎖,同時配合一個LIFO隊列來管理多餘的生產者和消費者,
            而後一種模式,如果生產者和消費者的處理速度有差距,則很容易出現飢渴的情況,即可能有某些生產者或者是消費者的數據永遠都得不到處理。

        小結:
              BlockingQueue不光實現了一個完整隊列所具有的基本功能,
            同時在多線程環境下,他還自動管理了多線間的自動等待於喚醒功能,
            從而使得程序員可以忽略這些細節,關注更高級的功能。



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