JUC源碼解析-阻塞隊列-LinkedBlockingQueue與ArrayBlockingQueue

什麼是阻塞隊列?

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

  • 在隊列爲空時,獲取元素的線程會阻塞等待,直到隊列變爲非空或超時。
  • 當隊列滿時,存儲元素的線程會等待隊列可用。

阻塞隊列常用於生產者和消費者的場景。

阻塞隊列提供了四種處理方法:
在這裏插入圖片描述

  • 拋出異常:是指當阻塞隊列滿時候,再往隊列裏插入元素,會拋出IllegalStateException(“Queue full”)異常。當隊列爲空時,從隊列裏獲取元素時會拋出NoSuchElementException異常 。
  • 返回特殊值:插入方法會返回是否成功,成功則返回true。移除方法,則是從隊列裏拿出一個元素,如果沒有則返回null
  • 一直阻塞:當阻塞隊列滿時,如果生產者線程往隊列裏put元素,隊列會一直阻塞生產者線程,直到拿到數據,或者響應中斷退出。當隊列空時,消費者線程試圖從隊列裏take元素,隊列也會阻塞消費者線程,直到隊列可用。
  • 超時退出:當阻塞隊列滿時,隊列會阻塞生產者線程一段時間,如果超過一定的時間,生產者線程就會退出。

ArrayBlockingQueue

ArrayBlockingQueue的內部是通過一個可重入鎖ReentrantLock和兩個Condition條件對象來實現阻塞。

關於 ReentrantLock
關於 Condition

public class ArrayBlockingQueue<E> extends AbstractQueue<E>
        implements BlockingQueue<E>, java.io.Serializable {

	// 存儲數據的數組
    final Object[] items;

	// 獲取數據的索引,用於take,poll,remove等方法
    int takeIndex;

	// 添加數據的索引,用於 put, offer, add等方法
    int putIndex;

	// 元素個數
    int count;

	// 鎖
    final ReentrantLock lock;

	// Condition 內有個隊連,有別於AQS的同步隊列,我們稱它爲等待隊列
	// await 會先構建當前線程的節點放入等待隊列,之後阻塞線程
	// signal 一般不會喚醒線程,而是將節點放回AQS的同步隊列,等待被喚醒
	
	// notEmpty隊列裏放的是執行獲取操作的線程,它們之前由於數組爲空無法執行獲取操作而阻塞
	// 執行取操作的線程由於數組爲空而被放入notEmpty的等待隊列中等待
	// 當執行添加操作的線程執行成功後調用notEmpty.signal將notEmpty隊列中的
	// 頭節點放回AQS的同步隊列,其代表的線程在隊列中等待被喚醒
    private final Condition notEmpty;

	// notFull 的隊列中放的是執行插入操作的線程,它們之前由於數組滿無法執行添加操作而阻塞
    private final Condition notFull;
    
    // 迭代器
    transient Itrs itrs = null;

在這裏插入圖片描述
在上面提到了AQS的同步隊列與Condition的等待隊列,它們在我的關於 ReentrantLockCondition兩篇源碼分析文章中有詳細介紹。

這裏貼張圖比那與理解:
在這裏插入圖片描述
第一條隊列是AQS的同步隊列,下面是Condition的等待隊列。

關於 notEmpty 與 notFull 不太好記憶,可以將 notEmpty 與 get 聯繫在一起;notFull 與 put 聯繫在一起。

add,offer添加操作

    public boolean add(E e) {
        return super.add(e);
    }
    
// 跳到AbstractQueue:
    public boolean add(E e) {
        if (offer(e))
            return true;
        else
            throw new IllegalStateException("Queue full");
    }
    
// offer方法在ArrayBlockingQueue的實現
    public boolean offer(E e) {
        checkNotNull(e);
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            if (count == items.length)
                return false;
            else {
                enqueue(e);
                return true;
            }
        } finally {
            lock.unlock();
        }
    }

    private void enqueue(E x) {
        // assert lock.getHoldCount() == 1;
        // assert items[putIndex] == null;
        final Object[] items = this.items;
        items[putIndex] = x;
        if (++putIndex == items.length)
            putIndex = 0;
        count++;
        notEmpty.signal();
    }

對重點進行下總結:

  • 添加操作先獲取鎖;
  • add 方法在數組滿時拋異常,offer 則返回 false
  • 喚醒一個notEmpty等待隊列中的線程,該隊列中的線程都是執行獲取操作的

put操作

    public void put(E e) throws InterruptedException {
        checkNotNull(e);
        final ReentrantLock lock = this.lock;
        // lockInterruptibly相對於lock,就如其名一樣,檢測到中斷就直接拋異常
        lock.lockInterruptibly();
        try {
            while (count == items.length)
            	// 數組滿無法插入,將插入線程放入notFull等待隊列
                notFull.await();
            enqueue(e);
        } finally {
            lock.unlock();
        }
    }

可以看出 put 有別於 add,數組滿就等待而不是拋異常。

remove,poll刪除操作

    public E remove() {
        E x = poll();
        if (x != null)
            return x;
        else
            throw new NoSuchElementException();
    }

    public E poll() {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            return (count == 0) ? null : dequeue();
        } finally {
            lock.unlock();
        }
    }

    private E dequeue() {
        // assert lock.getHoldCount() == 1;
        // assert items[takeIndex] != null;
        final Object[] items = this.items;
        @SuppressWarnings("unchecked")
        E x = (E) items[takeIndex];
        items[takeIndex] = null;
        if (++takeIndex == items.length)
            takeIndex = 0;
        count--;
        // takeIndex位置元素被刪除,需根據情況對迭代器鏈上所有迭代器進行處理
        if (itrs != null)
            itrs.elementDequeued();
        notFull.signal();
        return x;
    }
  • 數組爲空時,remove 拋異常,poll 返回 null 。
  • 刪除成功後將 notFull 中一個插入線程的節點放回同步隊列,在隊列中輪到它時就被喚醒執行插入操作,所謂輪到它指的是在同步隊列中排在它之前的節點都被一一喚醒。
  • 元素刪除後可能會對一些迭代器造成影響,這裏需要處理這種影響。

阻塞隊列的迭代器實現原理分析我專門寫了兩篇分析:
深入Java併發之阻塞隊列-迭代器(一)
深入Java併發之阻塞隊列-迭代器(二)

take 阻塞獲取

    public E take() throws InterruptedException {
        final ReentrantLock lock = this.lock;
        lock.lockInterruptibly(); // 中斷拋異常
        try {
            while (count == 0)
                notEmpty.await();
            return dequeue();
        } finally {
            lock.unlock();
        }
    }

首先要獲取鎖,數組爲空則放入 notEmpty 等待隊列。

element,peek

    public E element() {
        E x = peek();
        if (x != null)
            return x;
        else
            throw new NoSuchElementException();
    }

    public E peek() {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            return itemAt(takeIndex); // null when queue is empty
        } finally {
            lock.unlock();
        }
    }

數組爲空 peek 返回 null;element 則拋異常。

LinkedBlockingQueue

LinkedBlockingQueue內部分別使用了 takeLock 和 putLock 兩個鎖對併發進行控制,添加和刪除操作並不是互斥操作,可以同時進行,這樣也就可以大大提高吞吐量。

public class LinkedBlockingQueue<E> extends AbstractQueue<E>
        implements BlockingQueue<E>, java.io.Serializable {

節點

    static class Node<E> {
        E item;
        
        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;

    // take,poll等取操作的鎖
    private final ReentrantLock takeLock = new ReentrantLock();

    // 數組爲空無法執行取操作的線程被放入 notEmpty 隊列中等待
    private final Condition notEmpty = takeLock.newCondition();

    // put,offer等插入操作的鎖
    private final ReentrantLock putLock = new ReentrantLock();

    // 數組滿而無法執行插入操作的線程被放入到 notFull 隊列中等待
    private final Condition notFull = putLock.newCondition();

構造函數

    public LinkedBlockingQueue() {
        this(Integer.MAX_VALUE); // 默認鏈最大長度爲Integer.MAX_VALUE
    }
    
    public LinkedBlockingQueue(int capacity) {
        if (capacity <= 0) throw new IllegalArgumentException();
        this.capacity = capacity;
        last = head = new Node<E>(null);
    }

	public LinkedBlockingQueue(Collection<? extends E> c) {
	......

插入操作

add,offer

下面你會經常看見打了雙引號的 喚醒 兩字——“喚醒”:經過一開始的分析,AQS有個同步隊列,通過它得到的Condition對象也有一個等待隊列,await 就是線程放入等待隊列,signal 就是將等待隊列中的頭節點放回同步隊列尾,並非是喚醒線程,它在同步隊列中等待直到被喚醒。具體的分析在我的 Condition 文章中,鏈接在上面。

    public boolean add(E e) {
        if (offer(e))
            return true;
        else
            throw new IllegalStateException("Queue full");
    }

    public boolean offer(E e) {
        if (e == null) throw new NullPointerException();
        final AtomicInteger count = this.count;
        if (count.get() == capacity) // 數組滿直接返回false
            return false;
        int c = -1; // 開始時設爲-1,若插入成功c的值應>=0,以此來判定offer操作是否成功
        Node<E> node = new Node<E>(e);
        final ReentrantLock putLock = this.putLock;
        putLock.lock();
        try {
        	// 若數組滿不做任何處理,c仍未-1,最後會返回false。
            if (count.get() < capacity) {
                enqueue(node);
                c = count.getAndIncrement(); // 這裏返回的是增加之前的值
                if (c + 1 < capacity) // 插入後數組未滿
                    notFull.signal(); // “喚醒”一個插入線程
            }
        } finally {
            putLock.unlock();
        }
        // c等於0 說明本次插入之前數組爲空,則可鞥有不少獲取操作的線程都在阻塞等待,
        // 所以可以在這裏喚醒一個,其實並不一定會喚醒線程,很可能是將節點從
        // notEmpty 等待對隊列中放回 takeLock 的同步隊列。
        // 具體分析見我分析 Condition 的文章
        if (c == 0) 
            signalNotEmpty();
        return c >= 0;
    }

    private void enqueue(Node<E> node) {
        // assert putLock.isHeldByCurrentThread();
        // assert last.next == null;
        last = last.next = node;
    }
    
    private void signalNotEmpty() {
        final ReentrantLock takeLock = this.takeLock;
        takeLock.lock();
        try {
            notEmpty.signal();
        } finally {
            takeLock.unlock();
        }
    }

從上面可以看出這種雙鎖設計的好處,當前插入線程完成後“喚醒”下一個插入線程,跟取操作互不影響。代碼最後在檢測到此次插入前數組爲空的情況時,會“喚醒”一個取線程,防止 notEmpty 隊列中等待的取線程一直阻塞不被喚醒,當然無論是取還是插入,當其執行完後都會在”喚醒“下一個取或插入。

put

操作與上面基本相同,只是當數組滿時 阻塞 線程。

		......
			while (count.get() == capacity) { //數組滿就阻塞
                notFull.await();
            }
        ......

刪除操作

remove,poll

    public E remove() {
        E x = poll();
        if (x != null)
            return x;
        else
            throw new NoSuchElementException();
    }

    public E poll() {
        final AtomicInteger count = this.count;
        if (count.get() == 0) // 數組爲空 返回null
            return null;
        E x = null;
        int c = -1;
        final ReentrantLock takeLock = this.takeLock;
        takeLock.lock();
        try {
            if (count.get() > 0) {
                x = dequeue();
                //將數量減一,返回值是刪除之前的數量
                c = count.getAndDecrement();
                if (c > 1) // 代表刪除之後數組仍不爲空
                    notEmpty.signal(); // “喚醒”下一個取線程
            }
        } finally {
            takeLock.unlock();
        }
        // 代表刪除之前數組爲滿,則可能阻塞了不少插入線程,“喚醒”一個
        if (c == capacity) 
            signalNotFull();
        return x;
    }

    private E dequeue() {
        // assert takeLock.isHeldByCurrentThread();
        // assert head.item == null;
        Node<E> h = head;
        Node<E> first = h.next;
        h.next = h; // help GC
        head = first;
        E x = first.item;
        //將新頭節點的item置空,已經刪除沒必要再持有對其的引用,不利於回收
        first.item = null;
        return x;
    }

take 的實現與上面沒什麼不同,只是再數組爲空時阻塞。

獲取操作

    public E element() {
        E x = peek();
        if (x != null)
            return x;
        else
            throw new NoSuchElementException();
    }

    public E peek() {
        if (count.get() == 0)
            return null;
        final ReentrantLock takeLock = this.takeLock;
        takeLock.lock();
        try {
            Node<E> first = head.next;
            if (first == null)
                return null;
            else
                return first.item;
        } finally {
            takeLock.unlock();
        }
    }

實現很簡單。由於併發下 head 不斷變換,所以需要獲取取鎖以保證安全性。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章