深入剖析阻塞隊列BlockingQueue (詳解ArrayBlockingQueue和LinkedBlockingQueue及其應用)

前言

這篇博客南國主要講解關於Java中阻塞隊列的知識點,提到阻塞隊列(BlockingQueue)想必大家最先想到的是生產者-消費者,誠然這也是阻塞隊列最直接的應用場景。 本篇分爲四個章節,BlockingQueue簡介,常見的基本操作,常用的BlockingQueue實現類和應用demo。這裏針對BlockingQueue的應用南國主要寫了生產者-消費者的實現和線程通信的實現。前三個部分的基礎知識總結,很多內容參考了併發容器之BlockingQueue的敘述,結合自己的理解南國 在一些內容上做了增加和重新編輯。
話不多說,乾貨送上~

1. BlockingQueue簡介

在實際編程中,會經常使用到JDK中Collection集合框架中的各種容器類如實現List,Map,Queue接口的容器類,但是這些容器類基本上不是線程安全的,除了使用Collections可以將其轉換爲線程安全的容器,Doug Lea大師爲我們都準備了對應的線程安全的容器,如實現List接口的CopyOnWriteArrayList,實現Map接口的ConcurrentHashMap,實現Queue接口的ConcurrentLinkedQueue

在我們學習操作系統時遇到的一個最經典的"生產者-消費者"問題中,隊列通常被視作線程間操作的數據容器,這樣,可以對各個模塊的業務功能進行解耦,生產者將“生產”出來的數據放置在數據容器中,而消費者僅僅只需要在“數據容器”中進行獲取數據即可,這樣生產者線程和消費者線程就能夠進行解耦,只專注於自己的業務功能即可。阻塞隊列(BlockingQueue)被廣泛使用在“生產者-消費者”問題中,其原因是BlockingQueue提供了可阻塞的插入和移除的方法。當隊列容器已滿,生產者線程會被阻塞,直到隊列未滿;當隊列容器爲空時,消費者線程會被阻塞,直至隊列非空時爲止

2. 常見的基本操作

BlockingQueue基本操作總結如下:
在這裏插入圖片描述
BlockingQueue繼承於Queue接口,因此,對數據元素的基本操作有:

插入元素:
add(E e) :往隊列插入數據,當隊列滿時,插入元素時會拋出IllegalStateException異常;
offer(E e):當往隊列插入數據時,插入成功返回true,否則則返回false。當隊列滿時不會拋出異常;

刪除元素:
remove(Object o):從隊列中刪除數據,成功則返回true,否則爲false
poll:刪除數據,當隊列爲空時,返回null;

查看元素:
element:獲取隊頭元素,如果隊列爲空時則拋出NoSuchElementException異常;
peek:獲取隊頭元素,如果隊列爲空則拋出NoSuchElementException異常

接下來,南國講一下BlockingQueue具有的特殊操作

插入數據:
put:當阻塞隊列容量已經滿時,往阻塞隊列插入數據的線程會被阻塞,直至阻塞隊列已經有空餘的容量可供使用;
offer(E e, long timeout, TimeUnit unit):若阻塞隊列已經滿時,同樣會阻塞插入數據的線程,直至阻塞隊列已經有空餘的地方,與put方法不同的是,該方法會有一個超時時間,若超過當前給定的超時時間,插入數據的線程會退出;

刪除數據:
take():當阻塞隊列爲空時,獲取隊頭數據的線程會被阻塞;
poll(long timeout, TimeUnit unit):當阻塞隊列爲空時,獲取數據的線程會被阻塞,另外,如果被阻塞的線程超過了給定的時長,該線程會退出

3. 常用的BlockingQueue

實現BlockingQueue接口的有ArrayBlockingQueue, DelayQueue, LinkedBlockingDeque, LinkedBlockingQueue, LinkedTransferQueue, PriorityBlockingQueue, SynchronousQueue,而這幾種常見的阻塞隊列也是在實際編程中會常用的,下面對這幾種常見的阻塞隊列進行說明:

3.1. ArrayBlockingQueue

ArrayBlockingQueue是由數組實現的有界阻塞隊列。該隊列命令元素FIFO(先進先出)。因此,對頭元素head是隊列中存在時間最長的數據元素,而對尾數據tail則是當前隊列最新的數據元素。ArrayBlockingQueue可作爲“有界數據緩衝區”,生產者插入數據到隊列容器中,並由消費者提取。ArrayBlockingQueue一旦創建,容量不能改變。

當隊列容量滿時,嘗試將元素放入隊列將導致操作阻塞;嘗試從一個空隊列中取一個元素也會同樣阻塞。

ArrayBlockingQueue默認情況下不能保證線程訪問隊列的公平性,所謂公平性是指嚴格按照線程等待的絕對時間順序,即最先等待的線程能夠最先訪問到ArrayBlockingQueue。而非公平性則是指訪問ArrayBlockingQueue的順序不是遵守嚴格的時間順序,有可能存在,一旦ArrayBlockingQueue可以被訪問時,長時間阻塞的線程依然無法訪問到ArrayBlockingQueue。如果保證公平性,通常會降低吞吐量。如果需要獲得公平性的ArrayBlockingQueue,可採用如下代碼:

private static ArrayBlockingQueue<Integer> blockingQueue = new ArrayBlockingQueue<Integer>(10,true);

3.1.1 ArrayBlockingQueue的主要屬性

ArrayBlockingQueue的主要屬性如下:

/** The queued items */
final Object[] items;

/** items index for next take, poll, peek or remove */
int takeIndex;

/** items index for next put, offer, or add */
int putIndex;

/** Number of elements in the queue */
int count;

/*
 * Concurrency control uses the classic two-condition algorithm
 * found in any textbook.
 */

/** Main lock guarding all access */
final ReentrantLock lock;

/** Condition for waiting takes */
private final Condition notEmpty;

/** Condition for waiting puts */
private final Condition notFull;

從源碼中可以看出ArrayBlockingQueue內部是採用數組進行數據存儲的(屬性items),爲了保證線程安全,採用的是ReentrantLock lock,爲了保證可阻塞式的插入刪除數據利用的是Condition,當獲取數據的消費者線程被阻塞時會將該線程放置到notEmpty等待隊列中,當插入數據的生產者線程被阻塞時,會將該線程放置到notFull等待隊列中。而notEmpty和notFull等中要屬性在構造方法中進行創建:

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();
}

接下來,主要看看可阻塞式的put和take方法是怎樣實現的。

3.1.2 put方法詳解

put(E e)方法源碼如下:

public void put(E e) throws InterruptedException {
    checkNotNull(e);
    final ReentrantLock lock = this.lock;
    lock.lockInterruptibly();
    try {
		//如果當前隊列已滿,將線程移入到notFull等待隊列中
        while (count == items.length)
            notFull.await();
		//滿足插入數據的要求,直接進行入隊操作
        enqueue(e);
    } finally {
        lock.unlock();
    }
}

該方法的邏輯很簡單,當隊列已滿時(count == items.length)將線程移入到notFull等待隊列中,如果當前滿足插入數據的條件,就可以直接調用 enqueue(e)插入數據元素。enqueue方法源碼爲:

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();
}

enqueue方法的邏輯同樣也很簡單,先完成插入數據,即往數組中添加數據(items[putIndex] = x),然後通知被阻塞的消費者線程,當前隊列中有數據可供消費(notEmpty.signal())。

3.1.3 take方法詳解

take方法源碼如下:

public E take() throws InterruptedException {
    final ReentrantLock lock = this.lock;
    lock.lockInterruptibly();
    try {
		//如果隊列爲空,沒有數據,將消費者線程移入等待隊列中
        while (count == 0)
            notEmpty.await();
		//獲取數據
        return dequeue();
    } finally {
        lock.unlock();
    }
}

take方法也主要做了兩步:1. 如果當前隊列爲空的話,則將獲取數據的消費者線程移入到等待隊列中;2. 若隊列不爲空則獲取數據,即完成出隊操作dequeue。dequeue方法源碼爲:

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--;
    if (itrs != null)
        itrs.elementDequeued();
    //通知被阻塞的生產者線程
	notFull.signal();
    return x;
}

dequeue方法也主要做了兩件事情:1. 獲取隊列中的數據,即獲取數組中的數據元素((E) items[takeIndex]);2. 通知notFull等待隊列中的線程,使其由等待隊列移入到同步隊列中,使其能夠有機會獲得lock,並執行完成功退出。

從以上分析,可以看出put和take方法主要是通過condition的通知機制來完成可阻塞式的插入數據和獲取數據。在理解ArrayBlockingQueue後再去理解LinkedBlockingQueue就很容易了。

3.2. LinkedBlockingQueue

LinkedBlockingQueue是用鏈表實現的阻塞隊列,同樣滿足FIFO的特性。與ArrayBlockingQueue相比起來具有更高的吞吐量,爲了防止LinkedBlockingQueue容量迅速增,損耗大量內,通常在創建LinkedBlockingQueue對象時,會指定其大小(如果指定了大小,我們判定它爲有界隊列),如果未指定,容量等於Integer.MAX_VALUE(我們視它爲無界隊列)。查看它的構造方法:

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

3.2.1 LinkedBlockingQueue的主要屬性

LinkedBlockingQueue的主要屬性有:

/** Current number of elements */
private final AtomicInteger count = new AtomicInteger();

/**
 * Head of linked list.
 * Invariant: head.item == null
 */
transient Node<E> head;

/**
 * Tail of linked list.
 * Invariant: last.next == null
 */
private transient Node<E> last;

/** Lock held by take, poll, etc */
private final ReentrantLock takeLock = new ReentrantLock();

/** Wait queue for waiting takes */
private final Condition notEmpty = takeLock.newCondition();

/** Lock held by put, offer, etc */
private final ReentrantLock putLock = new ReentrantLock();

/** Wait queue for waiting puts */
private final Condition notFull = putLock.newCondition();

可以看出與ArrayBlockingQueue主要的區別是,LinkedBlockingQueue在插入數據和刪除數據時分別是由兩個不同的lock(takeLock和putLock)來控制線程安全的,因此,也由這兩個lock生成了兩個對應的condition(notEmpty和notFull)來實現可阻塞的插入和刪除數據。並且,採用了鏈表的數據結構來實現隊列,Node結點的定義爲:

static class Node<E> {
    E item;

    /**
     * One of:
     * - the real successor Node
     * - this Node, meaning the successor is head.next
     * - null, meaning there is no successor (this is the last node)
     */
    Node<E> next;

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

接下來,我們也同樣來看看put方法和take方法的實現。

3.2.2 put方法詳解

put方法源碼爲:

public void put(E e) throws InterruptedException {
    if (e == null) throw new NullPointerException();
    // Note: convention in all put/take/etc is to preset local var
    // holding count negative to indicate failure unless set.
    int c = -1;
    Node<E> node = new Node<E>(e);
    final ReentrantLock putLock = this.putLock;
    final AtomicInteger count = this.count;
    putLock.lockInterruptibly();
    try {
        /*
         * Note that count is used in wait guard even though it is
         * not protected by lock. This works because count can
         * only decrease at this point (all other puts are shut
         * out by lock), and we (or some other waiting put) are
         * signalled if it ever changes from capacity. Similarly
         * for all other uses of count in other wait guards.
         */
		//如果隊列已滿,則阻塞當前線程,將其移入等待隊列
        while (count.get() == capacity) {
            notFull.await();
        }
		//入隊操作,插入數據
        enqueue(node);
        c = count.getAndIncrement();
		//若隊列滿足插入數據的條件,則通知被阻塞的生產者線程
        if (c + 1 < capacity)
            notFull.signal();
    } finally {
        putLock.unlock();
    }
    if (c == 0)
        signalNotEmpty();
}

put方法的邏輯也同樣很容易理解,可見註釋。基本上和ArrayBlockingQueue的put方法一樣。

3.2.3 take方法的源碼如下:

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;
}

take方法的主要邏輯請見於註釋,也很容易理解。

3.2.4. ArrayBlockingQueue與LinkedBlockingQueue的比較(重要)

相同點:ArrayBlockingQueue和LinkedBlockingQueue都是通過condition通知機制來實現可阻塞式插入和刪除元素,並滿足線程安全的特性;

不同點:1. ArrayBlockingQueue底層是採用的數組進行實現,而LinkedBlockingQueue則是採用鏈表數據結構; 2. ArrayBlockingQueue插入和刪除數據,只採用了一個lock,而LinkedBlockingQueue則是在插入和刪除分別採用了putLock和takeLock,這樣可以降低線程由於線程無法獲取到lock而進入WAITING狀態的可能性,從而提高了線程併發執行的效率。

3.3. PriorityBlockingQueue

PriorityBlockingQueue是一個支持優先級的無界阻塞隊列。默認情況下元素採用自然順序進行排序,也可以通過自定義類實現compareTo()方法來指定元素排序規則,或者初始化時通過構造器參數Comparator來指定排序規則。

3.4. SynchronousQueue

它的實質是一種無緩衝的等待隊列。SynchronousQueue每個插入操作必須等待另一個線程進行相應的刪除操作,因此,SynchronousQueue實際上沒有存儲任何數據元素,因爲只有線程在刪除數據時,其他線程才能插入數據,同樣的,如果當前有線程在插入數據時,線程才能刪除數據。SynchronousQueue也可以通過構造器參數來爲其指定公平性。

  • 公平模式:SynchronousQueue採用公平鎖,並配合一個FIFO隊列來阻塞多餘的生產者和消費者。
  • 非公平模式:SynchronoueQueue採用非公平鎖,同時配合一個LIFO隊列來管理多餘的生產者和消費者。

3.5. LinkedTransferQueue

LinkedTransferQueue是一個由鏈表數據結構構成的無界阻塞隊列,由於該隊列實現了TransferQueue接口,與其他阻塞隊列相比主要有以下不同的方法:

  • transfer(E e) 如果當前有線程(消費者)正在調用take()方法或者可延時的poll()方法進行消費數據時,生產者線程可以調用transfer方法將數據傳遞給消費者線程。如果當前沒有消費者線程的話,生產者線程就會將數據插入到隊尾,直到有消費者能夠進行消費才能退出;
  • tryTransfer(E e) tryTransfer方法如果當前有消費者線程(調用take方法或者具有超時特性的poll方法)正在消費數據的話,該方法可以將數據立即傳送給消費者線程,如果當前沒有消費者線程消費數據的話,就立即返回false。因此,與transfer方法相比,transfer方法是必須等到有消費者線程消費數據時,生產者線程才能夠返回。而tryTransfer方法能夠立即返回結果退出。
  • tryTransfer(E e,long timeout,imeUnit unit)
    與transfer基本功能一樣,只是增加了超時特性,如果數據才規定的超時時間內沒有消費者進行消費的話,就返回false。

3.6. LinkedBlockingDeque

LinkedBlockingDeque是基於鏈表數據結構的有界阻塞雙端隊列,如果在創建對象時爲指定大小時,其默認大小爲Integer.MAX_VALUE。與LinkedBlockingQueue相比,主要的不同點在於,LinkedBlockingDeque具有雙端隊列的特性。LinkedBlockingDeque基本操作如下圖所示:
在這裏插入圖片描述
如上圖所示,LinkedBlockingDeque的基本操作可以分爲四種類型:1.特殊情況,拋出異常;2.特殊情況,返回特殊值如null或者false;3.當線程不滿足操作條件時,線程會被阻塞直至條件滿足;4. 操作具有超時特性。

另外,LinkedBlockingDeque實現了BlockingDueue接口而LinkedBlockingQueue實現的是BlockingQueue,這兩個接口的主要區別如下圖所示:
在這裏插入圖片描述
從上圖可以看出,兩個接口的功能是可以等價使用的,比如BlockingQueue的add方法和BlockingDeque的addLast方法的功能是一樣的。

3.7. DelayQueue

DelayQueue是一個存放實現Delayed接口的數據的無界阻塞隊列,只有當數據對象的延時時間達到時才能插入到隊列進行存儲。如果當前所有的數據都還沒有達到創建時所指定的延時期,則隊列沒有隊頭,並且線程通過poll等方法獲取數據元素則返回null。所謂數據延時期滿時,則是通過Delayed接口的getDelay(TimeUnit.NANOSECONDS)來進行判定,如果該方法返回的是小於等於0則說明該數據元素的延時期已滿。

4. 應用Demo

通過前面的學習,相比你已經對阻塞隊列以及常用的類型有了一個基本的瞭解。 下面,南國將兩個阻塞應用的最廣泛的例子:生產者-消費者模式的實現,實現線程通信

4.1 實現生產者-消費者模式

1. 拋開這篇博客提到的阻塞隊列,我們手動寫一個非阻塞隊列的方式實現消費者-生產者模式。

package 併發多線程.生產者_消費者模式;

import java.util.PriorityQueue;

/**
 * 使用Object.wait()和Object.notify()  非阻塞隊列的方式實現消費者-生產者模式
 *
 * @author xjh 2019.12.26
 */
public class Wait_Notify {
    private int queueSize = 10;
    private PriorityQueue<Integer> queue = new PriorityQueue<>(queueSize);

    public static void main(String[] args) {
        Wait_Notify wait_notify = new Wait_Notify();
        Producer producer =wait_notify.new Producer();  //內部類的對象創建,需要通過外部類對象進行調用
        Consumer consumer=wait_notify.new Consumer();
        producer.start();
        consumer.start();
    }

    //創建內部類 Producer表示生產者線程相關的類
    class Producer extends Thread {
        @Override
        public void run() {
            produce();
        }

        private void produce() {
            while (true) {
                synchronized (queue) {
                    //對代碼塊進行加鎖
                    while (queue.size() == queueSize) {    //隊列已滿,不能再生產了
                        System.out.println("the queue is full, please wait...");
                        try {
                            queue.wait();   //當前線程掛起,進入等待隊列
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                            queue.notify(); //notify 喚醒等待掛起的線程
                        }
                    }
                    queue.offer(1); //入隊一個元素
                    queue.notify();
                    System.out.println("I have inserted one element, the rest capacity is: " + (queueSize - queue.size()));
                }
            }
        }
    }

    //創建內部類 Consumer表示消費者線程相關的類
    class Consumer extends Thread {
        @Override
        public void run() {
            consume();
        }

        private void consume() {
            while (true) {
                synchronized (queue) {
                    //對代碼塊進行加鎖
                    while (queue.size() == 0) {    //隊列爲空,不能再消費了
                        System.out.println("the queue is empty, please wait...");
                        try {
                            queue.wait();   //當前線程掛起,進入等待隊列
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                            queue.notify(); //notify 喚醒等待掛起的線程
                        }
                    }
                    queue.poll(); //出隊一個元素
                    queue.notify();
                    System.out.println("I have polled one element, the rest elements are: " + queue.size());
                }
            }
        }
    }
}

輸出結果:

......
the queue is full, please wait...
I have polled one element, the rest elements are: 9
I have polled one element, the rest elements are: 8
I have polled one element, the rest elements are: 7
I have polled one element, the rest elements are: 6
I have polled one element, the rest elements are: 5
I have polled one element, the rest elements are: 4
I have polled one element, the rest elements are: 3
I have polled one element, the rest elements are: 2
I have polled one element, the rest elements are: 1
I have polled one element, the rest elements are: 0
the queue is empty, please wait...
I have inserted one element, the rest capacity is: 9
I have inserted one element, the rest capacity is: 8
I have inserted one element, the rest capacity is: 7
I have inserted one element, the rest capacity is: 6
I have inserted one element, the rest capacity is: 5
I have inserted one element, the rest capacity is: 4
I have inserted one element, the rest capacity is: 3
I have inserted one element, the rest capacity is: 2
I have inserted one element, the rest capacity is: 1
I have inserted one element, the rest capacity is: 0
.......

2. 使用阻塞隊列實現生產者-消費者(這裏我使用的是ArrayBlockingQueue)

package 併發多線程.生產者_消費者模式;

import java.util.concurrent.ArrayBlockingQueue;

/**
 * 使用ArrayBlockQueue實現生產者消費者模式
 * @author xjh 2019.12.26
 */
public class ArrayBlockQueue_Demo {
    private int queueSize = 10;
    private ArrayBlockingQueue<Integer> queue = new ArrayBlockingQueue<>(queueSize);
    // 非阻塞模式用的PriorityQueue,阻塞模式下我們使用ArrayBlockingQueue

    public static void main(String[] args) {
        ArrayBlockQueue_Demo arrayBlockQueue_demo = new ArrayBlockQueue_Demo();
        Producer producer=arrayBlockQueue_demo.new Producer();
        Consumer consumer=arrayBlockQueue_demo.new Consumer();

        consumer.start();
        producer.start();
    }

    //創建內部類 Producer表示生產者線程相關的類
    class Producer extends Thread {
        @Override
        public void run() {
            produce();
        }

        private void produce() {
            while (true) {
                try {
                    queue.put(1);
                    System.out.println("I have inserted one element, the rest capacity is: " + (queueSize - queue.size()));
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    //創建內部類 Consumer表示消費者線程相關的類
    class Consumer extends Thread {
        @Override
        public void run() {
            consume();
        }

        private void consume() {
            while (true) {
                try {
                    queue.take();
                    System.out.println("I have took one element, the rest elements are: " + queue.size());
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

輸出結果:

.........
I have took one element, the rest elements are: 9
I have took one element, the rest elements are: 8
I have took one element, the rest elements are: 8
I have took one element, the rest elements are: 7
I have took one element, the rest elements are: 6
I have took one element, the rest elements are: 5
I have took one element, the rest elements are: 4
I have took one element, the rest elements are: 3
I have took one element, the rest elements are: 2
I have took one element, the rest elements are: 1
I have took one element, the rest elements are: 0
I have inserted one element, the rest capacity is: 1
I have inserted one element, the rest capacity is: 9
I have inserted one element, the rest capacity is: 9
I have inserted one element, the rest capacity is: 8
I have inserted one element, the rest capacity is: 7
I have inserted one element, the rest capacity is: 6
I have inserted one element, the rest capacity is: 5
I have inserted one element, the rest capacity is: 4
I have inserted one element, the rest capacity is: 3
I have inserted one element, the rest capacity is: 2
I have inserted one element, the rest capacity is: 1
I have inserted one element, the rest capacity is: 0
.........

注意,這兩段代碼的結果都是截取的部分效果。 讀者相比發現了使用阻塞隊列代碼要簡單得多,不需要再單獨考慮同步和線程間通信的問題。
在併發編程中,一般推薦使用阻塞隊列,這樣實現可以儘量地避免程序出現意外的錯誤。

阻塞隊列使用最經典的場景就是socket客戶端數據的讀取和解析,讀取數據的線程不斷將數據放入隊列,然後解析線程不斷從隊列取數據解析。還有其他類似的場景,只要符合生產者-消費者模型的都可以使用阻塞隊列。

4.2 阻塞隊列實現線程通信(這裏我使用的是LinkedBlockingQueue)

package 併發多線程;

import java.util.concurrent.LinkedBlockingQueue;

/**
 * 使用BlockingQueue來實現線程通信
 * @author xjh 2019.09.09
 * 這裏我用了兩種玩法:
一種是共享一個queue,根據peek和poll的不同來實現;
第二種是兩個queue,利用take()會自動阻塞來實現。
 */
class MethodSeven {
    //1.共享一個queue,根據peek和poll的不同來實現;
    private final LinkedBlockingQueue<String> queue = new LinkedBlockingQueue<>();

    public Runnable newThreadOne() {
        final String[] inputArr = Helper.buildNoArr(52);
        return new Runnable() {
            private String[] arr = inputArr;

            public void run() {
                for (int i = 0; i < arr.length; i = i + 2) {
                    Helper.print(arr[i], arr[i + 1]);
                    queue.offer("TwoToGo");
                    while (!"OneToGo".equals(queue.peek())) {
                    }
                    queue.poll();
                }
            }
        };
    }

    public Runnable newThreadTwo() {
        final String[] inputArr = Helper.buildCharArr(26);
        return new Runnable() {
            private String[] arr = inputArr;

            public void run() {
                for (int i = 0; i < arr.length; i++) {
                    while (!"TwoToGo".equals(queue.peek())) {
                    }
                    queue.poll();
                    Helper.print(arr[i]);
                    queue.offer("OneToGo");
                }
            }
        };
    }

    //2.兩個queue,利用take()會自動阻塞來實現。
    private final LinkedBlockingQueue<String> queue1 = new LinkedBlockingQueue<>();
    private final LinkedBlockingQueue<String> queue2 = new LinkedBlockingQueue<>();

    public Runnable newThreadThree() {
        final String[] inputArr = Helper.buildNoArr(52);
        return new Runnable() {
            private String[] arr = inputArr;

            public void run() {
                for (int i = 0; i < arr.length; i = i + 2) {
                    Helper.print(arr[i], arr[i + 1]);
                    try {
                        queue2.put("TwoToGo");
                        queue1.take();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        };
    }

    public Runnable newThreadFour() {
        final String[] inputArr = Helper.buildCharArr(26);
        return new Runnable() {
            private String[] arr = inputArr;

            public void run() {
                for (int i = 0; i < arr.length; i++) {
                    try {
                        queue2.take();
                        Helper.print(arr[i]);
                        queue1.put("OneToGo");
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        };
    }
}

//創建一個枚舉類型
enum Helper {
    instance;
    private static final ExecutorService tPool = Executors.newFixedThreadPool(2);
    //數字
    public static String[] buildNoArr(int max) {
        String[] noArr = new String[max];
        for(int i=0;i<max;i++){
            noArr[i] = Integer.toString(i+1);
        }
        return noArr;
    }
    //字母
    public static String[] buildCharArr(int max) {
        String[] charArr = new String[max];
        int tmp = 65;
        for(int i=0;i<max;i++){
            charArr[i] = String.valueOf((char)(tmp+i));
        }
        return charArr;
    }

    public static void print(String... input){
        if(input==null)
            return;
        for(String each:input){
            System.out.print(each);
        }
    }
    public void run(Runnable r){
        tPool.submit(r);
    }
    public void shutdown(){
        tPool.shutdown();
    }
}
public class BlockingQueueTest {
    public static void main(String args[]) throws InterruptedException {
        MethodSeven seven = new MethodSeven();
        Helper.instance.run(seven.newThreadOne());
        Helper.instance.run(seven.newThreadTwo());
        Thread.sleep(2000);
        System.out.println("");
        Helper.instance.run(seven.newThreadThree());
        Helper.instance.run(seven.newThreadFour());
        Helper.instance.shutdown();
    }
}

輸出結果:

12A34B56C78D910E1112F1314G1516H1718I1920J2122K2324L2526M2728N2930O3132P3334Q3536R3738S3940T4142U4344V4546W4748X4950Y5152Z
12A34B56C78D910E1112F1314G1516H1718I1920J2122K2324L2526M2728N2930O3132P3334Q3536R3738S3940T4142U4344V4546W4748X4950Y5152Z

參考資料:

  1. 併發容器之BlockingQueue
  2. Java併發編程:阻塞隊列
發佈了65 篇原創文章 · 獲贊 20 · 訪問量 1萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章