佇列在多執行緒中使用
1. 概述:
1.1 佇列簡介
佇列是一種特殊的線性表,特殊之處在於它只允許在表的前端(front)進行刪除操作,而在表的後端(rear)進行插入操作,和棧一樣,佇列是一種操作受限制的線性表。進行插入操作的端稱為隊尾,進行刪除操作的端稱為隊頭。佇列中沒有元素時,稱為空佇列。
1.2 佇列基本運算:
佇列操作 | 函式 | 初始條件 | 操作結果 |
---|---|---|---|
初始化佇列 | Init_Queue(q) | 隊q不存在 | 構造了一個空隊 |
入隊操作 | In_Queue(q,x) | 隊q存在 | 對已存在的佇列q,插入一個元素x到隊尾,隊發生變化 |
出隊操作 | Out_Queue(q,x) | 隊q存在且非空 | 刪除隊首元素,並返回其值,隊發生變化 |
讀隊頭 | Front_Queue(q,x) | 隊q存在且非空 | 讀隊頭元素,並返回其值,隊不變 |
判隊空操作 | Empty_Queue(q) | 隊q存在 | 若q為空隊則返回為1,否則返回為0。 |
2. 佇列在多執行緒中的使用
佇列以一種先進先出的方式管理資料,如果你試圖向一個 已經滿了的阻塞佇列中新增一個元素或者是從一個空的阻塞佇列中移除一個元索,將導致執行緒阻塞.在多執行緒進行合作時,阻塞佇列是很有用的工具。工作者執行緒可以定期地把中間結果存到阻塞佇列中而其他工作者線執行緒把中間結果取出並在將來修改它們。佇列會自動平衡負載。如果第一個執行緒集執行得比第二個慢,則第二個 執行緒集在等待結果時就會阻塞。如果第一個執行緒集執行得快,那麼它將等待第二個執行緒集趕上來。
Java提供的執行緒安全的Queue可以分為阻塞佇列和非阻塞佇列,其中阻塞佇列的典型例子是BlockingQueue,非阻塞佇列的典型例子是ConcurrentLinkedQueue,在實際應用中要根據實際需要選用阻塞佇列或者非阻塞佇列。
3. 阻塞佇列:BlockingQueue
一)BlockingQueue提供的常用方法
操作 | 可能報異常 | 返回布林值 | 可能阻塞 | 設定等待時 |
---|---|---|---|---|
入隊 | add(e) | offer(e) | put(e) | offer(e,timeout,unit) |
出隊 | remove() | poll() | take() | poll(timeout, unit) |
檢視 | element() | peek() | 無 | 無 |
從上表可以很明顯看出每個方法的作用,這個不用多說。這裡強排程的是:
1) add(e) remove() element() 方法不會阻塞執行緒。當不滿足約束條件時,會丟擲IllegalStateException異常。例如:當佇列被元素填滿後,再呼叫add(e),則會丟擲異常。
2) offer(e) poll() peek() 方法即不會阻塞執行緒,也不會丟擲異常。例如:當佇列被元素填滿後,再呼叫offer(e),則不會插入元素,函式返回false。
3) 要想要實現阻塞功能,需要呼叫put(e) take() 方法。當不滿足約束條件時,會阻塞執行緒。
二)BlockingQueue介面的具體實現類:
ArrayBlockingQueue,其建構函式必須帶一個int引數來指明其大小, LinkedBlockingQueue,若其建構函式帶一個規定大小的引數,生成的BlockingQueue有大小限制,若不帶大小引數,所生成的BlockingQueue的大小由Integer.MAX_VALUE來決定,PriorityBlockingQueue,其所含物件的排序不是FIFO,而是依據物件的自然排序順序或者是建構函式的Comparator決定的順序
4. 非阻塞佇列:ConcurrentLinkedQueue
ConcurrentLinkedQueue是一個無鎖的併發執行緒安全的佇列。對比鎖機制的實現,使用無鎖機制的難點在於要充分考慮執行緒間的協調。簡單的說就是多個執行緒對內部資料結構進行訪問時,如果其中一個執行緒執行的中途因為一些原因出現故障,其他的執行緒能夠檢測並幫助完成剩下的操作。這就需要把對資料結構的操作過程精細的劃分成多個狀態或階段,考慮每個階段或狀態多執行緒訪問會出現的情況。
ConcurrentLinkedQueue有兩個volatile的執行緒共享變數:head,tail。要保證這個佇列的執行緒安全就是保證對這兩個Node的引用的訪問(更新,檢視)的原子性和可見性,由於volatile本身能夠保證可見性,所以就是對其修改的原子性要被保證。
ConcurrentLinkedQueue有兩個volatile的執行緒共享變數:head,tail。要保證這個佇列的執行緒安全就是保證對這兩個Node的引用的訪問(更新,檢視)的原子性和可見性,由於volatile本身能夠保證可見性,所以就是對其修改的原子性要被保證
佇列總是處於兩種狀態之一:正常狀態(或稱靜止狀態,圖 1 和圖 3)或中間狀態(圖 2)。在插入操作之前和第二個 CAS(D)成功之後,佇列處在靜止狀態;在第一個 CAS(C)成功之後,佇列處在中間狀態。在靜止狀態時,尾指標指向的連結節點的 next 欄位總為 null,而在中間狀態時,這個欄位為非 null。任何執行緒通過比較 tail.next 是否為 null,就可以判斷出佇列的狀態,這是讓執行緒可以幫助其他執行緒 “完成” 操作的關鍵
插入操作在插入新元素(A)之前,先檢查佇列是否處在中間狀態。如果是在中間狀態,那麼肯定有其他執行緒已經處在元素插入的中途,在步驟(C)和(D)之間。不必等候其他執行緒完成,當前執行緒就可以“幫助” 它完成操作,把尾指標向前移動(B)。如果有必要,它還會繼續檢查尾指標並向前移動指標,直到佇列處於靜止狀態,這時它就可以開始自己的插入了。
第一個 CAS(C)可能因為兩個執行緒競爭訪問隊列當前的最後一個元素而失敗;在這種情況下,沒有發生修改,失去 CAS 的執行緒會重新裝入尾指標並再次嘗試。如果第二個 CAS(D)失敗,插入執行緒不需要重試 —— 因為其他執行緒已經在步驟(B)中替它完成了這個操作!
上圖顯示的是:處在插入中間狀態的佇列,在新元素插入之後,尾指標更新之前
上圖顯示的是:在尾指標更新後,佇列重新處在靜止狀態