java-阻塞隊列

概要

1.什麼是阻塞隊列
2.幾種主要的阻塞隊列
3.阻塞隊列中的方法 VS 非阻塞隊列中的方法
4.阻塞隊列的實現原理
5.示例和使用場景

1.什麼是阻塞隊列

阻塞隊列(BlockingQueue)是一個支持兩個附加操作的隊列。這兩個附加的操作是:
    1.在隊列爲空時,獲取元素的線程會等待隊列變爲非空。
    2.當隊列滿時,存儲元素的線程會等待隊列可用。 

    阻塞隊列常用於生產者和消費者的場景,生產者是往隊列裏添加元素的線程,消費者是從隊列裏拿元素的
線程。阻塞隊列就是生產者存放元素的容器,而消費者也只從容器裏拿元素。

1.1簡單的阻塞隊列代碼
import java.util.PriorityQueue;

public class Test2 {
    private int queueSize = 10;
    private PriorityQueue<Integer> queue = new PriorityQueue<Integer>(queueSize);

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

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

    class Consumer extends Thread{

        @Override
        public void run() {
            consume();
        }

        private void consume() {
            while(true){
                synchronized (queue) {
                    while(queue.size() == 0){
                        try {
                            System.out.println("隊列空,等待數據");
                            queue.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                            queue.notify();
                        }
                    }
                    queue.poll();          //每次移走隊首元素
                    queue.notify();
                    System.out.println("從隊列取走一個元素,隊列剩餘"+queue.size()+"個元素");
                }
            }
        }
    }

    class Producer extends Thread{

        @Override
        public void run() {
            produce();
        }

        private void produce() {
            while(true){
                synchronized (queue) {
                    while(queue.size() == queueSize){
                        try {
                            System.out.println("隊列滿,等待有空餘空間");
                            queue.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                            queue.notify();
                        }
                    }
                    queue.offer(1);        //每次插入一個元素
                    queue.notify();
                    System.out.println("向隊列取中插入一個元素,隊列剩餘空間:"+(queueSize-queue.size()));
                }
            }
        }
    }
}

2.幾種主要的阻塞隊列

1. ArrayBlockingQueue :一個由數組結構組成的有界阻塞隊列。
2. LinkedBlockingQueue :一個由鏈表結構組成的有界阻塞隊列。
3. PriorityBlockingQueue :一個支持優先級排序的無界阻塞隊列。
4. DelayQueue:一個使用優先級隊列實現的無界阻塞隊列。
5. SynchronousQueue:一個不存儲元素的阻塞隊列。
6. LinkedTransferQueue:一個由鏈表結構組成的無界阻塞隊列。
7. LinkedBlockingDeque:一個由鏈表結構組成的雙向阻塞隊列。

3.阻塞隊列中的方法 VS 非阻塞隊列中的方法

這裏寫圖片描述

1、往隊列中添加元素的方法有4鍾,分別爲:add(e)/offer(e)/put(e)/offer(e,time,unit)

2、往隊列中取元素的方法有4種,分別爲:remove()/poll()/take()/poll(long,TimeUnit).

3、檢查隊列中的元素有2種,分別爲:element()/peek().

文檔:https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/BlockingQueue.html    

4.阻塞隊列的實現原理

4.1 ArrayBlockingQueue
4.1.1 ArrayBlockingQueue繼承體系結構
public class ArrayBlockingQueue<E> extends AbstractQueue<E>
        implements BlockingQueue<E>, java.io.Serializable {
4.1.2 ArrayBlockingQueue的相關屬性
    final Object[] items;
    int takeIndex;
    int putIndex;
    int count;
    final ReentrantLock lock;
    private final Condition notEmpty;
    private final Condition notFull;
ArrayBlockingQueue是基於數組,所以有一個數組items來保存元素。關鍵字synchronized與wait()和
notify()/notifyAll()方法相結合可以實現等待/通知模式。但是Condition可以實現多路通知,有選擇性
地進行線程通知。
4.1.3 ArrayBlockingQueue的構造方法
 //創建一個指定大小的隊列對象。
    public ArrayBlockingQueue(int capacity) {
        this(capacity, false);
    }

    public ArrayBlockingQueue(int capacity, boolean fair) {
        if (capacity <= 0)
            throw new IllegalArgumentException();
        this.items = new Object[capacity];
        lock = new ReentrantLock(fair);//NonfairSync(悲觀鎖)
        notEmpty = lock.newCondition();
        notFull =  lock.newCondition();
    }
    ReentrantLock.lock函數中,會調用到NonfairSync.lock方法,首先會通過CAS方法,嘗試將當前
的AQS中的State字段改成從0改成1,如果修改成功的話,說明原來的狀態是0,並沒有線程佔用鎖,而且成功
的獲取了鎖,只需要調用setExclusiveOwnerThread函將當前線程設置成持有鎖的線程即可。否則,CAS操
作失敗之後,和普通鎖一樣,調用acquire(1)函數嘗試獲取鎖。
4.1.4 ArrayBlockingQueue類中的put方法
public void put(E e) throws InterruptedException {
        checkNotNull(e);
        final ReentrantLock lock = this.lock;//獲取鎖
        lock.lockInterruptibly();//如果當前線程未被中斷,則獲取鎖定,如果已經被中斷則出現異常。還有lock/tryLock。
        try {
            //檢查是否已滿,如果已滿,則調用Condition的await方法等待並釋放鎖
            while (count == items.length)
                notFull.await();
            enqueue(e);//如果沒滿,則直接加入到隊列中
        } finally {
            lock.unlock();//最後釋放鎖
        }
    }

    private static void checkNotNull(Object v) {
        if (v == null)
            throw new NullPointerException();
    }
    private void enqueue(E x) {
        final Object[] items = this.items;
        items[putIndex] = x;
        if (++putIndex == items.length)
            putIndex = 0;
        count++;
        //喚醒一個消費者
        notEmpty.signal();
    }
如果滿了調用notFull.await(),如果有增加元素,喚醒notEmpty.signal()。take()和put()在這方面
相反。
4.1.5 ArrayBlockingQueue類中的take方法
public E take() throws InterruptedException {
        final ReentrantLock lock = this.lock;
        lock.lockInterruptibly();
        try {
            while (count == 0)
                notEmpty.await();//如果隊列中存儲的元素爲空,則等待直至隊列中非空,put()中被喚醒。
            return dequeue();
        } finally {
            lock.unlock();
        }
    }
    private E dequeue() {
        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();//喚醒put()中等待的生產者
        return x;
    }
4.2 LinkedBlockingQueue
4.2.1 LinkedBlockingQueue繼承體系結構
 public class LinkedBlockingQueue<E> extends AbstractQueue<E>
            implements BlockingQueue<E>, java.io.Serializable 
4.2.2 ArrayBlockingQueue的相關屬性
    static class Node<E> {
        E item;

        Node<E> next;

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

    private final int capacity;//容量,默認Integer.MAX_VALUE
private final AtomicInteger count = new AtomicInteger(); 當前隊列中存儲元素的數量
    transient Node<E> head;

    private transient Node<E> last;

    private final ReentrantLock takeLock = new ReentrantLock();
    private final ReentrantLock putLock = new ReentrantLock();//take和put的鎖,ArrayBlockingQueue是通用一個,因爲數組不能同時put和take,但是鏈表可以。

    private final Condition notEmpty = takeLock.newCondition();
    private final Condition notFull = putLock.newCondition();//同樣的2個鎖和Condition。
ArrayBlockingQueue是共用一個,而LinkedBlockingQueue採用了兩把鎖,一個是take鎖,一個是put鎖,這就說明,put和take操作是可以同時進行的。
4.2.3 LinkedBlockingQueue的構造方法
 public LinkedBlockingQueue() {
        this(Integer.MAX_VALUE);
    }

    public LinkedBlockingQueue(int capacity) {
        if (capacity <= 0) throw new IllegalArgumentException();
        this.capacity = capacity;
        last = head = new Node<E>(null);//頭尾結點指向一個節點,存儲的元素爲null
    }
4.2.4 LinkedBlockingQueue的put方法介紹
public void put(E e) throws InterruptedException {
        if (e == null) throw new NullPointerException();
        int c = -1;//注意:用此局部變量持有一個負數來指示CAS操作是否操作成功。 c = count.getAndIncrement();//利用原子性加一
        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();
            }
            enqueue(node);
            c = count.getAndIncrement();//利用原子性加一
            if (c + 1 < capacity)//如果當前存儲的元素個數小於容量,則喚醒正在等待的生產者的線程。
                notFull.signal();
        } finally {
            putLock.unlock();
        }
        if (c == 0)//第一次添加了一個元素,因此需要喚醒一個消費者線程
            signalNotEmpty();
    }
    /*
     * 將節點加入到鏈表的末尾
     */
    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();
        }
    }
4.2.5 LinkedBlockingQueue的take方法介紹
public E take() throws InterruptedException {
        E x;
        int c = -1;
        final AtomicInteger count = this.count;
        final ReentrantLock takeLock = this.takeLock;
        takeLock.lockInterruptibly();//加take鎖
        try {
            //如果當前隊列爲空,則一直等待直到隊列非空
            while (count.get() == 0) {
                notEmpty.await();
            }
            x = dequeue();//將頭結點取出來
            c = count.getAndDecrement();//利用原子性進行進行減一操作
            //如果此時的容量是大於1的,則說明還有其它元素可以被消費線程獲取,因此喚醒其他消費者線程()
            if (c > 1)
                notEmpty.signal();
        } finally {
            takeLock.unlock();//釋放鎖
        }
        //如果c的容量是等於capacity,又被消費了一個,因此可以通知生產者線程來進行生產
        if (c == capacity)
            signalNotFull();
        return x;
    }

    private E dequeue() {
        Node<E> h = head;
        Node<E> first = h.next;
        h.next = h; 
        head = first;
        E x = first.item;
        first.item = null;
        return x;
    }


    /**
     * 喚醒一個生產者線程
     */
    private void signalNotFull() {
        final ReentrantLock putLock = this.putLock;
        putLock.lock();
        try {
            notFull.signal();
        } finally {
            putLock.unlock();
        }
    }
4.3 PriorityBlockingQueue
一個支持優先級排序的無界阻塞隊列。PriorityBlockingQueue需要插入其中的元素類型提供一個
Comparator,PriorityBlockingQueue使用這個Comparator來確定元素之間的優先級關係,底層是基於
一個二叉堆的實現。
4.4 PriorityQueue
支持延時獲取元素的無界阻塞隊列,即可以指定多久才能從隊列中獲取當前元素。常見的例子比如使用一個
DelayQueue來管理一個超時未響應的連接隊列。
4.5 SynchronousQueue
不存儲元素的阻塞隊列,每一個put必須等待一個take操作,否則不能繼續添加元素。

5.示例和使用場景

5.1示例
public class Test
{
    private int queueSize = 10;
    private ArrayBlockingQueue<Integer> queue = new ArrayBlockingQueue<Integer>(queueSize);
    public static void main(String[] args)
    {
        Test test = new Test();
        Producer producer = test.new Producer();
        Consumer consumer = test.new Consumer();
        producer.start();
        consumer.start();
    }
    class Consumer extends Thread
    {
        @Override
        public void run()
        {
            consume();
        }

        private void consume()
        {
            while (true)
            {
                try
                {
                    queue.take();
                    System.out.println("從隊列取走一個元素,隊列剩餘" + queue.size() + "個元素");
                } catch (InterruptedException e)
                {
                    e.printStackTrace();
                }
            }
        }
    }
    class Producer extends Thread
    {
        @Override
        public void run()
        {
            produce();
        }
        private void produce()
        {
            while (true)
            {
                try
                {
                    queue.put(1);
                    System.out.println("向隊列取中插入一個元素,隊列剩餘空間:"+ (queueSize - queue.size()));
                } catch (InterruptedException e)
                {
                    e.printStackTrace();
                }
            }
        }
    }
}
5.2應用場景
5.2.1 FixedThreadPool
    public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
    }

這裏寫圖片描述

1.它是一種固定大小的線程池;
2.corePoolSize和maximunPoolSize都爲用戶設定的線程數量nThreads;
3.keepAliveTime爲0,意味着一旦有多餘的空閒線程,就會被立即停止掉;
4.但這裏keepAliveTime無效;阻塞隊列採用了LinkedBlockingQueue,它是一個無界隊列;
5.由於阻塞隊列是一個無界隊列,因此永遠不可能拒絕任務;
6.由於採用了無界隊列,實際線程數量將永遠維持在nThreads,因此maximumPoolSize和keepAliveTime將無效。
5.2.2 FixedThreadPool
    public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());
    }

這裏寫圖片描述

它是一個可以無限擴大的線程池;
它比較適合處理執行時間比較小的任務;
corePoolSize爲0,maximumPoolSize爲無限大,意味着線程數量可以無限大;
keepAliveTime爲60S,意味着線程空閒時間超過60S就會被殺死;
採用SynchronousQueue裝等待的任務,這個阻塞隊列沒有存儲空間,這意味着只要有請求到來,就必須要找到一條工作線程處理他,如果當前沒有空閒的線程,那麼就會再創建一條新的線程。

參考資料:
https://www.zhihu.com/question/23212914
http://blog.csdn.net/u010412719/article/details/52337471
https://www.jianshu.com/p/f7d05d06ef54

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