Java 中常見的阻塞隊列有哪些?
文章目錄
前言
BlockingQueue 接口的實現類都在 J.U.C (java.util.concurrent)包中,本章將介紹以下 5 種常見的實現類
- ArrayBlockingQueue
- LinkedBlockingQueue
- SynchronousQueue
- PriorityBlockingQueue
- DelayQueue
項目環境
- jdk 1.8
- github 地址:https://github.com/huajiexiewenfeng/java-concurrent
- 本章模塊:blockingqueue
1.ArrayBlockingQueue
ArrayBlockingQueue 是最典型的有界隊列,其內部是用數組存儲元素的,利用 ReentrantLock 實現線程安全,使用 Condition 來阻塞和喚醒線程
我們在創建它的時候就需要指定它的容量,之後也不可以再擴容了,在構造函數中我們同樣可以指定是否是公平的,代碼如下:
public ArrayBlockingQueue(int capacity, boolean fair) {
if (capacity <= 0)
throw new IllegalArgumentException();
this.items = new Object[capacity];
lock = new ReentrantLock(fair);
notEmpty = lock.newCondition();
notFull = lock.newCondition();
}
第一個參數是容量,第二個參數是是否公平。和 ReentrantLock 一樣,如果 ArrayBlockingQueue 被設置爲非公平的,那麼就存在插隊的可能;如果設置爲公平的,那麼等待了最長時間的線程會被優先處理,其他線程不允許插隊,不過這樣的公平策略同時會帶來一定的性能損耗,因爲非公平的吞吐量通常會高於公平的情況。
2.LinkedBlockingQueue
從命名可以看出,這是一個內部用鏈表實現的 BlockingQueue。如果我們不指定它的初始容量,那麼它容量默認就爲整型的最大值 Integer.MAX_VALUE,由於這個數非常大,我們通常不可能放入這麼多的數據,所以 LinkedBlockingQueue 也被稱作無界隊列,代表它幾乎沒有界限。
其他特點:
- 同樣也利用 ReentrantLock 實現線程安全,使用 Condition 來阻塞和喚醒線程
- 無法設置 ReentrantLock 的公平非公平,默認是非公平
- 也可以設置固定大小
默認無參構造函數如下,默認最大值 Integer.MAX_VALUE:
public LinkedBlockingQueue() {
this(Integer.MAX_VALUE);
}
3.SynchronousQueue
SynchronousQueue 最大的不同之處在於,它的容量爲 0,所以沒有一個地方來暫存元素,導致每次取數據都要先阻塞,直到有數據被放入;同理,每次放數據的時候也會阻塞,直到有消費者來取。
需要注意的是,SynchronousQueue 的容量不是 1 而是 0,因爲 SynchronousQueue 不需要去持有元素,它所做的就是直接傳遞(direct handoff)。由於每當需要傳遞的時候,SynchronousQueue 會把元素直接從生產者傳給消費者,在此期間並不需要做存儲,所以如果運用得當,它的效率是很高的。
爲什麼說它的容量是 0 ,我們可以看其中的幾個方法:
- peek 方法永遠返回 null,代碼如下:
public E peek() {
return null;
}
因爲 peek 方法的含義是取出頭結點,但是 SynchronousQueue 的容量是 0,所以連頭結點都沒有,peek 方法也就沒有意義,所以始終返回 null。
- 同理,element 方法始終會拋出 NoSuchElementException 異常,但是這個方法的實現在它的父類 AbstractQueue 中
public E element() {
E x = peek();
if (x != null)
return x;
else
throw new NoSuchElementException();
}
- SynchronousQueue 的 size 方法始終返回 0,因爲它內部並沒有容量,代碼如下:
public int size() {
return 0;
}
4.PriorityBlockingQueue
ArrayBlockingQueue 和 LinkedBlockingQueue 都是採用先進先出的順序進行排序,可是如果有的時候我們需要自定義排序怎麼辦呢?這時就需要使用 PriorityBlockingQueue。
PriorityBlockingQueue 是一個支持優先級的無界阻塞隊列,可以通過自定義類實現 compareTo() 方法來指定元素排序規則,或者初始化時通過構造器參數 Comparator 來指定排序規則。同時,插入隊列的對象必須是可比較大小的,也就是 Comparable 的,否則會拋出 ClassCastException 異常。
帶構 Comparator 參數的造函數如下:
public PriorityBlockingQueue(int initialCapacity,
Comparator<? super E> comparator) {
if (initialCapacity < 1)
throw new IllegalArgumentException();
this.lock = new ReentrantLock();
this.notEmpty = lock.newCondition();
this.comparator = comparator;
this.queue = new Object[initialCapacity];
}
它的 take 方法在隊列爲空的時候會阻塞,但是正因爲它是無界隊列,而且會自動擴容,所以它的隊列永遠不會滿,所以它的 put 方法永遠不會阻塞,添加操作始終都會成功,也正因爲如此,它的成員變量裏只有一個 Condition:
private final Condition notEmpty;
這和之前的 ArrayBlockingQueue 擁有兩個 Condition(分別是 notEmpty 和 notFull)形成了鮮明的對比,我們的 PriorityBlockingQueue 不需要 notFull,因爲它永遠都不會滿。
示例
public class PriorityBlockingQueueDemo {
public static void main(String[] args) {
System.out.println("=====ArrayBlockingQueue=====");
BlockingQueue<Node> arrayBlockingQueue = new ArrayBlockingQueue<>(10);
arrayBlockingQueue.offer(new Node(2));
arrayBlockingQueue.offer(new Node(3));
arrayBlockingQueue.offer(new Node(1));
for (int i = 0; i < 3; i++) {
System.out.println(arrayBlockingQueue.poll().toString());
}
System.out.println("=====PriorityBlockingQueue=====");
BlockingQueue<Node> priorityBlockingQueue = new PriorityBlockingQueue<>(10, new Node());
priorityBlockingQueue.offer(new Node(2));
priorityBlockingQueue.offer(new Node(3));
priorityBlockingQueue.offer(new Node(1));
for (int i = 0; i < 3; i++) {
Node node = priorityBlockingQueue.poll();
System.out.println(node.toString());
}
}
static class Node implements Comparator<Node> {
private Integer value;
public Node() {
}
public Node(Integer value) {
this.value = value;
}
public Integer getValue() {
return value;
}
public void setValue(Integer value) {
this.value = value;
}
@Override
public int compare(Node o1, Node o2) {
return o1.getValue() - o2.getValue();
}
@Override
public String toString() {
return "Node{" +
"value=" + value +
'}';
}
}
}
執行結果:
=====ArrayBlockingQueue=====
Node{value=2}
Node{value=3}
Node{value=1}
=====PriorityBlockingQueue=====
Node{value=1}
Node{value=2}
Node{value=3}
可以看到 PriorityBlockingQueue 隊列取出的元素是經過排序的。
5.DelayQueue
DelayQueue 這個隊列比較特殊,具有“延遲”的功能。我們可以設定讓隊列中的任務延遲多久之後執行,比如 10 秒鐘之後執行,這在例如“30 分鐘後未付款自動取消訂單”等需要延遲執行的場景中被大量使用。
它是無界隊列,放入的元素必須實現 Delayed 接口,而 Delayed 接口又繼承了 Comparable 接口,所以自然就擁有了比較和排序的能力,Delayed 接口代碼如下:
public interface Delayed extends Comparable<Delayed> {
/**
* Returns the remaining delay associated with this object, in the
* given time unit.
*
* @param unit the time unit
* @return the remaining delay; zero or negative values indicate
* that the delay has already elapsed
*/
long getDelay(TimeUnit unit);
}
可以看出這個 Delayed 接口繼承自 Comparable。這裏的 getDelay 方法返回的是“還剩下多長的延遲時間纔會被執行”,如果返回 0 或者負數則代表任務已過期。元素會根據延遲時間的長短被放到隊列的不同位置,越靠近隊列頭代表越早過期,DelayQueue 內部使用了 PriorityQueue 的能力來進行排序。
示例:
public class DelayQueueDemo {
private static final long NANO_ORIGIN = System.nanoTime();
public static void main(String[] args) throws InterruptedException {
BlockingQueue<DelayedNode> delayQueue = new DelayQueue<>();
delayQueue.offer(new DelayedNode(2, 4));
delayQueue.offer(new DelayedNode(1, 2));
for (int i = 0; i < 10; i++) {
System.out.printf("時間[%d]\n", i + 1);
Thread.sleep(1000);
System.out.println(delayQueue.poll());
}
}
static class DelayedNode implements Delayed {
private Integer value;
private long time;// 秒
DelayedNode(Integer value, long time) {
this.value = value;
this.time = time;
}
public Integer getValue() {
return value;
}
@Override
public long getDelay(TimeUnit unit) {
return time - unit.toSeconds(delayTime());
}
private static long delayTime() {
return System.nanoTime() - NANO_ORIGIN;
}
@Override
public int compareTo(Delayed o) {
if (o instanceof DelayedNode) {
return value - ((DelayedNode) o).getValue();
} else {
return 0;
}
}
@Override
public String toString() {
return "DelayedNode{" +
"value=" + value +
'}';
}
}
}
執行結果:
時間[1]
null
時間[2]
DelayedNode{value=1}
時間[3]
null
時間[4]
DelayedNode{value=2}
時間[5]
null
可以看到第 2 秒的時候,才取到第一個值 1,因爲我們讓值爲 1 的元素,延遲 2 秒執行;同理第 4 秒的時候,我們取到值爲 2 的元素。
6.參考
- 《Java 併發編程 78 講》- 徐隆曦