答:
Concurrent 類型基於 lock-free,基於CAS的無鎖技術,在常見的多線程訪問場景,一般可以提供較高吞吐量。
而 LinkedBlockingQueue 內部則是基於鎖,並提供了 BlockingQueue 的等待性方法。
,java.util.concurrent 包提供的容器
大概區分爲 Concurrent*、CopyOnWrite和 Blocking等三類
Concurrent 類型沒有類似 CopyOnWrite 之類容器相對較重的修改開銷。Concurrent 往往提供了較低的遍歷一致性。即的弱一致性,當利用迭代器遍歷時,如果容器發生修改,迭代器仍然可以繼續進行遍歷,且弱一致性,size 等操作準確性是有限的,未必是 100% 準確。讀取的性能具有一定的不確定性。
與弱一致性對應的,就是我介紹過的同步容器常見的行爲“fail-fast”,也就是檢測到容器在遍歷過程中發生了修改,則拋出 ConcurrentModificationException,不再繼續遍歷。
Deque 的側重點是支持對隊列頭尾都進行插入和刪除,尾部插入時需要的addLast(e)、offerLast(e)。尾部刪除所需要的removeLast()、pollLast()。
Blocking 意味着其提供了特定的等待性操作,獲取時(take)等待元素進隊,或者插入時(put)等待隊列出現空位。
哪些隊列是有界的,哪些是無界的:
ConcurrentLinkedQueue是一個基於鏈接節點的無界線程安全隊列
ArrayBlockingQueue 是最典型的的有界隊列,其內部以 final 的數組保存數據,數組的大小就決定了隊列的邊界,所以我們在創建 ArrayBlockingQueue 時,都要指定容量。
LinkedBlockingQueue,容易被誤解爲無邊界,但其實其行爲和內部代碼都是基於有界的邏輯實現的,只不過如果我們沒有在創建隊列時就指定容量,那麼其容量限制就自動被設置爲 Integer.MAX_VALUE,成爲了無界隊列。
SynchronousQueue,這是一個非常奇葩的隊列實現,每個刪除操作都要等待插入操作,反之每個插入操作也都要等待刪除動作。那麼這個隊列的容量是多少呢?是 1 嗎?其實不是的,其內部容量是 0。
PriorityBlockingQueue 是無邊界的優先隊列,雖然嚴格意義上來講,其大小總歸是要受系統資源影響。
DelayedQueue 和 LinkedTransferQueue 同樣是無邊界的隊列。對於無邊界的隊列,有一個自然的結果,就是 put 操作永遠也不會發生其他 BlockingQueue 的那種等待情況。
SynchronousQueue,在 Java 6 中,其實現發生了非常大的變化,利用 CAS 替換掉了原本基於鎖的邏輯,同步開銷比較小。它是 Executors.newCachedThreadPool() 的默認隊列。
Queue 被廣泛使用在生產者 - 消費者場景,BlockingQueue 來實現,由於其提供的等待機制,我們可以少操心很多協調工作
生產者消費者模式:
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
public class ConsumerProducer {
public static final String EXIT_MSG = "Good bye!";
public static void main(String[] args) {
// 使用較小的隊列,以更好地在輸出中展示其影響
BlockingQueue<String> queue = new ArrayBlockingQueue<>(3);
Producer producer = new Producer(queue);
Consumer consumer = new Consumer(queue);
new Thread(producer).start();
new Thread(consumer).start();
}
static class Producer implements Runnable {
private BlockingQueue<String> queue;
public Producer(BlockingQueue<String> q) {
this.queue = q;
}
@Override
public void run() {
for (int i = 0; i < 20; i++) {
try{
Thread.sleep(5L);
String msg = "Message" + i;
System.out.println("Produced new item: " + msg);
queue.put(msg);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
try {
System.out.println("Time to say good bye!");
queue.put(EXIT_MSG);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
static class Consumer implements Runnable{
private BlockingQueue<String> queue;
public Consumer(BlockingQueue<String> q){
this.queue=q;
}
@Override
public void run() {
try{
String msg;
while(!EXIT_MSG.equalsIgnoreCase( (msg = queue.take()))){
System.out.println("Consumed item: " + msg);
Thread.sleep(10L);
}
System.out.println("Got exit message, bye!");
}catch(InterruptedException e) {
e.printStackTrace();
}
}
}
}
實際開發中如何選擇這些隊列:
對比
queue | 阻塞與否 | 是否有界 | 線程安全保障 | 適用場景 | 注意事項 |
---|---|---|---|---|---|
ArrayBlockingQueue | 阻塞 | 有界 | 一把全局鎖 | 生產消費模型,平衡兩邊處理速度 | -- |
LinkedBlockingQueue | 阻塞 | 可配置 | 存取採用2把鎖 | 生產消費模型,平衡兩邊處理速度 | 無界的時候注意內存溢出問題 |
ConcurrentLinkedQueue | 非阻塞 | 無界 | CAS | 對全局的集合進行操作的場景 | size() 是要遍歷一遍集合,慎用 |
考慮應用場景中對隊列邊界的要求。ArrayBlockingQueue 是有明確的容量限制的,而 LinkedBlockingQueue 則取決於我們是否在創建時指定,SynchronousQueue 則乾脆不能緩存任何元素。
從空間利用角度,數組結構的 ArrayBlockingQueue 要比 LinkedBlockingQueue 緊湊,因爲其不需要創建所謂節點,但是其初始分配階段就需要一段連續的空間,所以初始內存需求更大。
通用場景中,LinkedBlockingQueue 的吞吐量一般優於 ArrayBlockingQueue,因爲它實現了更加細粒度的鎖操作。ArrayBlockingQueue 實現比較簡單,性能更好預測,屬於表現穩定的“選手”。
SynchronousQueue 的性能表現,往往大大超過其他實現,尤其是在隊列元素較小的場景,還可代替CountDownLatch實現的是兩個線程之間接力性(handoff)的場景。
指定某種結構,比如棧,用它實現一個 BlockingQueue,實現思路是怎樣的呢?
總共3個棧,其中2個寫入棧(A、B),1個消費棧棧C(消費數據),但是有1個寫入棧是空閒的棧(B),隨時等待寫入,當消費棧(C)中數據爲空的時候,消費線程(await),觸發數據轉移,原寫入棧(A)停止寫入,,由空閒棧(B)接受寫入的工作,原寫入棧(A)中的數據轉移到消費棧(C)中,轉移完成後繼續(sign)繼續消費,2個寫入棧,1個消費棧優點是:不會堵塞寫入,但是消費會有暫停