【JUC阻塞隊列】ArrayBlockingQueue和LinkedBlockingQueue

目錄

Java中的阻塞隊列

1 ArrayBlockingQueue

1.1 成員變量

1.2 構造方法

1.3 put方法

1.4 take方法

2 LinkedBlockingQueue

2.1 成員變量

2.2 構造方法

2.3 put方法

2.4 take方法

3 ArrayBlockingQueue和LinkedBlockingQueue的區別


Java中的阻塞隊列

阻塞隊列(BlockingQueue)是一個支持以下兩個附加操作的隊列:

  • 支持阻塞的插入方法:當隊列滿時,隊列會阻塞插入元素的線程,直到隊列不滿。
  • 支持阻塞的移除方法:在隊列爲空時,獲取元素的線程會等待隊列變爲非空。

阻塞隊列常用於生產者和消費者的場景,生產者是向隊列裏添加元素的線程,消費者是從隊列裏取元素的線程。阻塞隊列就是生產者用來存放元素、消費者用來獲取元素的容器。

在阻塞隊列不可用時,這兩個附加操作提供了以下4種處理方式:

方法/處理方式 拋出異常 返回特殊值 一直阻塞 超時退出
插入方法 add(e) offer(e) put(e) offer(e, time, unit)
移除方法 remove() poll() take() poll(time, unit)
檢查方法 element() peek() 不可用 不可用
  • 拋出異常:隊列滿時,再添加元素,會拋出IllegalStateException("Queue full")異常;當隊列爲空時,從隊列裏獲取元素會拋出NoSuchElementException異常。
  • 返回特殊值:往隊列插入元素時,返回ture表示插入成功。從隊列裏移除元素,即取出元素,如果沒有則返回null
  • 一直阻塞:當阻塞隊列滿時,如果生產者線程往隊列裏put元素,隊列會一直阻塞生產者線程,直到隊列可用或者響應中斷退出。當隊列空時,如果消費者線程從隊列裏take元素,隊列會阻塞住消費者線程,直到隊列不爲空。
  • 超時退出:當阻塞隊列滿時,如果生產者線程往隊列裏插入元素,隊列會阻塞生產者線程一段時間,如果超過了指定的時間time,生產者線程就會退出。

如果是無界阻塞隊列,隊列不可能會出現滿的情況,所以使用putoffer方法永遠不會被阻塞,而且使用offer方法時,該方法永遠返回true

1 ArrayBlockingQueue

ArrayBlockingQueue是一個有界隊列,採用數組存儲數據,遵循FIFO原則,隊列的頭元素是隊列中存在時間最長的元素,尾結點是存在時間最短的元素。ArrayBlockingQueue在創建時指定容量,一旦創建不可更改。當隊列滿時,入隊操作線程會阻塞;當隊列空時,出隊操作線程會阻塞。ArrayBlockingQueue也支持公平和非公平的入隊和出隊,通過ReentrantLock的公平機制來實現。

1.1 成員變量

使用ReentrantLock來對生產和消費動作進行加鎖,使它們互斥執行。同時使用ReentrantLock的兩個Condition對生產者和消費者線程進行等待/喚醒。

    // 數據容器
    final Object[] items;

    // 要消費的下一個元素的索引
    int takeIndex;

    // 要生產的下一個位置的索引
    int putIndex;

    // 隊列中元素個數
    int count;

    // 控制生產和消費互斥的獨佔鎖
    final ReentrantLock lock;

    // 用來對消費者進行等待/喚醒操作的對象
    private final Condition notEmpty;

    // 用來對生產者進行等待/喚醒操作的對象
    private final Condition notFull;

1.2 構造方法

構造ArrayBlockingQueue時必須要指定容量capacity,即數組items的長度,一旦創建,不能修改容量。可以傳入boolean參數來指定上面ReentrantLock的鎖的類型(true爲公平鎖,false爲非公平鎖),默認是非公平鎖。可以傳入一個集合,對items進行數據初始化插入。

    public ArrayBlockingQueue(int capacity, boolean fair,Collection<? extends E> c) {
        ......
    }

1.3 put方法

就是加鎖,然後將元素插入到items中。

    public void put(E e) throws InterruptedException {
        checkNotNull(e);
        final ReentrantLock lock = this.lock;
        lock.lockInterruptibly();
        try {    
            while (count == items.length)        // 隊列滿,線程等待在notFull上
                notFull.await();
            enqueue(e);
        } finally {
            lock.unlock();
        }
    }

    private void enqueue(E x) {
        final Object[] items = this.items;
        items[putIndex] = x;
        if (++putIndex == items.length)        
            putIndex = 0;                        // 回到items[0],循環
        count++;
        notEmpty.signal();                       // 喚醒一個等待在notEmpty上的消費者線程  
    }

1.4 take方法

就是加鎖,然後將元素從items中出隊。

    public E take() throws InterruptedException {
        final ReentrantLock lock = this.lock;
        lock.lockInterruptibly();
        try {
            while (count == 0)
                notEmpty.await();            // 隊列滿,線程等待在notEmpty上
            return dequeue();
        } finally {
            lock.unlock();
        }
    }

    private E dequeue() {
        final Object[] items = this.items;
        E x = (E) items[takeIndex];
        items[takeIndex] = null;
        if (++takeIndex == items.length)
            takeIndex = 0;
        count--;
        if (itrs != null)
            itrs.elementDequeued();        
        notFull.signal();                    // 喚醒一個等待在notFull上的生產者線程  
        return x;
    }

2 LinkedBlockingQueue

LinkedBlockingQueue是一個可選擇長度的有界隊列,採用鏈表存儲數據,不指定長度就是Integer.MAX_VALUE,遵循FIFO原則,隊列的頭元素是隊列中存在時間最長的元素,尾結點是存在時間最短的元素。當隊列滿時,入隊操作線程會阻塞;當隊列空時,出隊操作線程會阻塞。LinkedBlockingQueue使用兩把鎖分別對生產和消費操作進行同步。相較於ArrayBlockingQueue,LinkedBlockingQueue有更高的吞吐量,但在大部分應用中有更多的不確定性。(jdk原文)

2.1 成員變量

與ArrayBlockingQueue的不同之處在於,LinkedBlockingQueue使用了兩把鎖分別控制消費和生產。注意count是AtomicInteger類型的。

    static class Node<E> {        // 鏈表節點
        E item;

        // 若next等於自己,表示該node是head
        // 若next等於null,表示該node是tail
        // 否則next表示該node的後繼節點
        Node<E> next;

        Node(E x) { item = x; }
    }

    // 鏈表長度,沒指定爲Integer.MAX_VALUE
    private final int capacity;

    // 鏈表中節點數量,是一個原子類
    private final AtomicInteger count = new AtomicInteger();

    // 頭結點
    transient Node<E> head;

    // 尾結點
    private transient Node<E> last;

    // 消費鎖
    private final ReentrantLock takeLock = new ReentrantLock();

    // 用來對消費者進行等待/喚醒操作的對象
    private final Condition notEmpty = takeLock.newCondition();

    // 生產鎖
    private final ReentrantLock putLock = new ReentrantLock();

    // 用來對生產者進行等待/喚醒操作的對象
    private final Condition notFull = putLock.newCondition();

2.2 構造方法

可以指定鏈表長度,指定長度就是Integer.MAX_VALUE。顯然,LinkedBlockingQueue默認採用非公平鎖。

    public LinkedBlockingQueue(int capacity) {
        if (capacity <= 0) throw new IllegalArgumentException();
        this.capacity = capacity;
        last = head = new Node<E>(null);
    }

    public LinkedBlockingQueue() {
        this(Integer.MAX_VALUE);
    }

    public LinkedBlockingQueue(Collection<? extends E> c) {
        this(Integer.MAX_VALUE);
        // 將c中數據插入鏈表中
    }

2.3 put方法

    public void put(E e) throws InterruptedException {
        if (e == null) throw new NullPointerException();
        int c = -1;
        Node<E> node = new Node<E>(e);
        final ReentrantLock putLock = this.putLock;
        final AtomicInteger count = this.count;
        putLock.lockInterruptibly();
        try {
            while (count.get() == capacity) {    // 判斷容量是否滿了
                notFull.await();                // 當前線程等待在notFull上
            }
            enqueue(node);
            c = count.getAndIncrement();        // 原子更新count,一直阻塞在此,直到更新成功
            if (c + 1 < capacity)
                notFull.signal();                // 喚醒下一個生產者線程
        } finally {
            putLock.unlock();
        }
        if (c == 0)
            signalNotEmpty();                    // 若插入後count爲1,則喚醒一個消費者
    }

2.4 take方法

take方法與put方法基本一樣,只是判斷條件和操作不一樣。

    public E take() throws InterruptedException {
        E x;
        int c = -1;
        final AtomicInteger count = this.count;
        final ReentrantLock takeLock = this.takeLock;
        takeLock.lockInterruptibly();
        try {
            while (count.get() == 0) {
                notEmpty.await();
            }
            x = dequeue();
            c = count.getAndDecrement();
            if (c > 1)
                notEmpty.signal();
        } finally {
            takeLock.unlock();
        }
        if (c == capacity)
            signalNotFull();
        return x;
    }

3 ArrayBlockingQueue和LinkedBlockingQueue的區別

  • ArrayBlockingQueue中生產和消費都是使用同一個鎖;LinkedBlockingQueue中使用兩個鎖,生產使用putLock,消費使用takeLock。
  • ArrayBlockingQueue生產時直接將數據插入到數組中;LinkedBlockingQueue則要先將數據包裝成Node,再插入鏈表中,多創建了一個對象。
  • ArrayBlockingQueue在創建的時候長度必須制定,且創建後不能修改;LinkedBlockingQueue在創建時可以指定長度,也可不指定,不指定長度爲Integer.MAX_VALUE。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章