標籤: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)---阻塞隊列