Java 中常見的阻塞隊列有哪些?

Java 中常見的阻塞隊列有哪些?

前言

BlockingQueue 接口的實現類都在 J.U.C (java.util.concurrent)包中,本章將介紹以下 5 種常見的實現類

  • ArrayBlockingQueue
  • LinkedBlockingQueue
  • SynchronousQueue
  • PriorityBlockingQueue
  • DelayQueue

項目環境

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 講》- 徐隆曦
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章