LinkedBlockingQueue原理分析---基于JDK8

1.常用的阻塞队列 

1)ArrayBlockingQueue:规定大小的BlockingQueue,其构造函数必须带一个int参数来指明其大小.其所含的对象是以FIFO(先入先出)顺序排序的.

2)LinkedBlockingQueue:大小不定的BlockingQueue,若其构造函数带一个规定大小的参数,生成的BlockingQueue有大小限制,若不带大小参数,所生成的BlockingQueue的大小由Integer.MAX_VALUE来决定.其所含的对象是以FIFO(先入先出)顺序排序的

3)PriorityBlockingQueue:类似于LinkedBlockQueue,但其所含对象的排序不是FIFO,而是依据对象的自然排序顺序或者是构造函数的Comparator决定的顺序.

4)SynchronousQueue:特殊的BlockingQueue,对其的操作必须是放和取交替完成的.

其中LinkedBlockingQueue和ArrayBlockingQueue比较起来,它们背后所用的数据结构不一样,导致LinkedBlockingQueue的数据吞吐量要大于ArrayBlockingQueue,但在线程数量很大时其性能的可预见性低于ArrayBlockingQueue


2.LinkedBlockingQueue原理

  1. 基于链表实现,线程安全的阻塞队列。
  2. 使用锁分离方式提高并发,双锁(ReentrantLock):takeLock、putLock,允许读写并行,remove(e)和contain()、clear()需要同时获取2个锁。
  3. FIFO先进先出模式。
  4. 在大部分并发场景下,LinkedBlockingQueue的吞吐量比ArrayBlockingQueue更好,双锁,入队和出队同时进行
  5. 根据构造传入的容量大小决定有界还是无界,默认不传的话,大小Integer.Max


3.LinkedBlockingQueue的几个关键属性

static class Node {
        E item;

        /**后继节点
         */
        Node next;

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

    /** 队列容量,默认最大,可指定大小 */
    private final int capacity;

    /** 当前容量 */
    private final AtomicInteger count = new AtomicInteger();

    /**
     * 头节点.
     * Invariant: head.item == null
     */
    transient Node head;

    /**
     * 尾节点.
     * Invariant: last.next == null
     */
    private transient Node last;

    /** 定义的出队和入队分离锁,2个队列空和满的出队和入队条件 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();

构造函数:默认是队列,可指定为有界,或初始给于一个初始集合数据


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

    /**
     * 指定有界大小,同时初始化head和tail节点
     *
     * @param capacity the capacity of this queue
     * @throws IllegalArgumentException if {@code capacity} is not greater
     *         than zero
     */
    public LinkedBlockingQueue(int capacity) {
        if (capacity <= 0) throw new IllegalArgumentException();
        this.capacity = capacity;
        last = head = new Node(null);
    }

    /**
     * 遍历集合元素,放到队列进行初始化  ---  无界队列
     *
     * @param c the collection of elements to initially contain
     * @throws NullPointerException if the specified collection or any
     *         of its elements are null
     */
    public LinkedBlockingQueue(Collection c) {
        this(Integer.MAX_VALUE);
        final ReentrantLock putLock = this.putLock;
        putLock.lock(); // Never contended, but necessary for visibility
        try {
            int n = 0;
            for (E e : c) {
                if (e == null)
                    throw new NullPointerException();
                if (n == capacity)
                    throw new IllegalStateException("Queue full");
                enqueue(new Node(e));
                ++n;
            }
            count.set(n);
        } finally {
            putLock.unlock();
        }
    }

4.BlockingQueue源码分析

    //入队,将元素添加到对尾等价  last.next = node; last = last.next
    private void enqueue(Node node) {
        last = last.next = node;
    }

    /**
     * 出队,从头部出
     *
     * @return the node
     */
    private E dequeue() {
        // assert takeLock.isHeldByCurrentThread();
        // assert head.item == null;
        Node h = head;
        Node first = h.next;
        h.next = h; // help GC
        head = first;
        E x = first.item;
        first.item = null;
        return x;
    }


// 队列已满:false  
public boolean offer(E e) {  
    if (e == null) throw new NullPointerException();  
    final AtomicInteger count = this.count;  
    if (count.get() == capacity) // 队列容量达到最大值,添加失败  
        return false;  
    int c = -1;  
    Node node = new Node(e);  
    final ReentrantLock putLock = this.putLock;  
    putLock.lock(); // 获取插入锁putLock  
    try {  
        if (count.get() < capacity) { // 加锁后再次判断队列是否已满  
            enqueue(node); // 入队  
            c = count.getAndIncrement(); // 返回Inc之前的值  
            if (c + 1 < capacity) // 插入节点后队列未满  
                notFull.signal(); // 唤醒notFull上的等待线程  
        }  
    } finally {  
        putLock.unlock(); // 释放插入锁  
    }  
    if (c == 0)  
        signalNotEmpty(); // 如果offer前队列为空,则唤醒notEmpty上的等待线程  
    return c >= 0;  
}  



public boolean offer(E e, long timeout, TimeUnit unit) throws InterruptedException 方法和 offer(E e)代码和功能均相似,但是如果在指定时间内未插入成功则会返回false。
比offer(E e)多的部分代码分析:
long nanos = unit.toNanos(timeout);  //将指定的时间长度转换为毫秒来进行处理  
while (count.get() == capacity) {  
    if (nanos <= 0) // 等待的剩余时间小于等于0,那么直接返回false  
        return false;  
    nanos = notFull.awaitNanos(nanos); // 最多等待时间(纳秒)  
}  


//插入节点:\n线程入队操作前会获取putLock锁,插入数据完毕后释放;
队列未满将新建Node节点,添加到队列末尾;
队列已满则阻塞线程(notFull.await())或返回false;若线程B取出数据,则会调用notFull.signal()唤醒notFull上的等待线程(线程A继续插数据)。
若入队前队列为空,则唤醒notEmpty上等待的获取数据的线程
// 一直阻塞直到插入成功  
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 node = new Node(e);  
    final ReentrantLock putLock = this.putLock;  
    final AtomicInteger count = this.count;  
 // 可中断的锁获取操作(优先考虑响应中断),如果线程由于获取锁而处于Blocked状态时,线程将被中断而不再继续等待(throws InterruptedException),可避免死锁。  
    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循环可避免“伪唤醒”(线程被唤醒时队列大小依旧达到最大值)  
        while (count.get() == capacity) {  
            notFull.await(); // notFull:入队条件  
        }  
        enqueue(node); // 将node链接到队列尾部  
        c = count.getAndIncrement(); // 元素入队后队列元素总和  
        if (c + 1 < capacity) // 队列未满  
            notFull.signal(); // 唤醒其他执行入队列的线程  
    } finally {  
        putLock.unlock(); // 释放锁  
    }  
// c=0说明队列之前为空,出队列线程均处于等待状态。添加一个元素后,队列已不为空,于是唤醒等待获取元素的线程  
    if (c == 0)  
        signalNotEmpty();  
}  


获取方法
先看几个重要方法:
[java] view plain copy
  1. /** 
  2.  * 唤醒等待插入数据的线程. Called only from take/poll. 
  3.  */  
  4. private void signalNotFull() {  
  5.     final ReentrantLock putLock = this.putLock;  
  6.     putLock.lock();  
  7.     try {  
  8.         notFull.signal();  
  9.     } finally {  
  10.         putLock.unlock();  
  11.     }  
  12. }  
  13. /** 
  14. * 队列头部元素出队. 
  15. * 
  16. * @return the node 
  17. */  
  18. private E dequeue() {  
  19.     // assert takeLock.isHeldByCurrentThread();  
  20.     // assert head.item == null;  
  21.     Node h = head; // 临时变量h  
  22.     Node first = h.next;  
  23.     h.next = h; // 形成环引用help GC  
  24.     head = first;  
  25.     E x = first.item;  
  26.     first.item = null;  
  27.     return x;  
  28. }  

4.1、 poll()
[java] view plain copy
  1. // 队列为空返回null而不是抛异常  
  2. public E poll() {  
  3.     final AtomicInteger count = this.count;  
  4.     if (count.get() == 0)  
  5.         return null;  
  6.     E x = null;  
  7.     int c = -1;  
  8.     final ReentrantLock takeLock = this.takeLock;  
  9.     takeLock.lock();  
  10.     try {  
  11.         if (count.get() > 0) {  
  12.             x = dequeue();  
  13.             c = count.getAndDecrement(); // 减1并返回旧值  
  14.             if (c > 1)  
  15.                 notEmpty.signal(); // 唤醒其他取数据的线程  
  16.         }  
  17.     } finally {  
  18.         takeLock.unlock();  
  19.     }  
  20.  // c等于capacity说明poll之前队列已满,poll一个元素后便可唤醒其他等待插入数据的线程  
  21.     if (c == capacity)  
  22.         signalNotFull();  
  23.     return x;  
  24. }  

衍生方法:
// 为poll方法增加了时间限制,指定时间未取回数据则返回null
[java] view plain copy
  1. public E poll(long timeout, TimeUnit unit)throws InterruptedException{}  

4.2、 take()
// 一直阻塞直到取回数据
[java] view plain copy
  1. public E take() throws InterruptedException {  
  2.     E x;  
  3.     int c = -1;  
  4.     final AtomicInteger count = this.count;  
  5.     final ReentrantLock takeLock = this.takeLock;  
  6.     takeLock.lockInterruptibly();  
  7.     try {  
  8.         while (count.get() == 0) { // 队列为空,一直等待  
  9.             notEmpty.await();  
  10.         }  
  11.         x = dequeue(); // 出队  
  12.         c = count.getAndDecrement();  
  13.         if (c > 1// take数据前队列大小大于1,则take后队列至少还有1个元素  
  14.             notEmpty.signal(); // 唤醒其他取数据的线程  
  15.     } finally {  
  16.         takeLock.unlock();  
  17.     }  
  18.     if (c == capacity)  
  19.         signalNotFull(); //唤醒其他等待插入数据的线程  
  20.     return x;  
  21. }  

4.3、drainTo(Collection c, int maxElements)
// 移除最多maxElements 个元素并将其加入集合
[java] view plain copy
  1. public int drainTo(Collectionsuper E> c, int maxElements) {  
  2.     if (c == null)  
  3.         throw new NullPointerException();  
  4.     if (c == this)  
  5.         throw new IllegalArgumentException();  
  6.     if (maxElements <= 0)  
  7.         return 0;  
  8.     boolean signalNotFull = false;  
  9.     final ReentrantLock takeLock = this.takeLock;  
  10.     takeLock.lock();  
  11.     try {  
  12.         int n = Math.min(maxElements, count.get());//转移元素数量不能超过队列总量   
  13.         // count.get provides visibility to first n Nodes  
  14.         Node h = head;  
  15.         int i = 0;  
  16.         try {  
  17.             while (i < n) {  
  18.                 Node p = h.next;//从队首获取元素  
  19.                 c.add(p.item);  
  20.                 p.item = null;//p为临时变量,置null方便GC  
  21.                 h.next = h;  
  22.                 h = p;  
  23.                 ++i;  
  24.             }  
  25.             return n;  
  26.         } finally {  
  27.             // Restore invariants even if c.add() threw  
  28.             if (i > 0) { // 有数据被转移到集合c中  
  29.                 // assert h.item == null;  
  30.                 head = h;  
  31.  //如果转移前的队列大小等于队列容量,则说明现在队列未满  
  32.  // 更新count为队列实际大小(减去i得到)  
  33.                 signalNotFull = (count.getAndAdd(-i) == capacity);  
  34.             }  
  35.         }  
  36.     } finally {  
  37.         takeLock.unlock();  
  38.         if (signalNotFull)  
  39.             signalNotFull(); // 唤醒其他等待插入数据的线程  
  40.     }  
  41. }  

衍生方法:
// 将[所有]可用元素加入集合c
[java] view plain copy
  1.  public int drainTo(Collectionsuper E> c) {  
  2.     return drainTo(c, Integer.MAX_VALUE);  
  3. }  

4.4、boolean retainAll(Collection c)
// 仅保留集合c中包含的元素,队列因此请求而改变则返回true
[java] view plain copy
  1. public boolean retainAll(Collection c) {  
  2.     Objects.requireNonNull(c); // 集合为null则throw NPE  
  3.     boolean modified = false;  
  4.     Iterator it = iterator();  
  5.     while (it.hasNext()) {  
  6.         if (!c.contains(it.next())) {  
  7.             it.remove();  
  8.             modified = true// 队列因此请求而改变则返回true  
  9.         }  
  10.     }  
  11.     return modified;  
  12. }  

LinkedBlockingQueue取数据小结:
线程A取数据前会获取takeLock锁,取完数据后释放锁。
队列有数据则(通常)返回队首数据;
若队列为空,则阻塞线程(notEmpty.await())或返回null等;当线程B插入数据后,会调用notEmpty.signal()唤醒notEmpty上的等待线程(线程A继续取数据)。
若取数据前队列已满,则通过notFull.signal()唤醒notFull上等待插入数据的线程。

5、检测方法(取回但不移除)
5.1、E peek()
// 返回队列头,队列为空返回null
[java] view plain copy
  1. public E peek() {  
  2.     if (count.get() == 0)  
  3.         return null;  
  4.     final ReentrantLock takeLock = this.takeLock;  
  5.     takeLock.lock();  
  6.     try {  
  7.         Node first = head.next;  
  8.         if (first == null)  
  9.             return null;  
  10.         else  
  11.             return first.item;  
  12.     } finally {  
  13.         takeLock.unlock();  
  14.     }  
  15. }  

6、综述
6.1、LinkedBlockingQueue通过对 插入、取出数据 使用不同的锁,实现多线程对竞争资源的互斥访问

6.2、(之前队列为空)添加数据后调用signalNotEmpty()方法唤醒等待取数据的线程;(之前队列已满)取数据后调用signalNotFull()唤醒等待插入数据的线程。这种唤醒模式可节省线程等待时间。

6.3、个别操作需要调用方法fullyLock()同时获取putLock、takeLock两把锁(如方法:clear()、contains(Object o)、remove(Object o)、toArray()、toArray(T[] a)、toString()),注意fullyLock和fullyUnlock获取锁和解锁的顺序刚好相反,避免死锁。
[java] view plain copy
  1. /** 
  2.  * Locks to prevent both puts and takes. 
  3.  */  
  4. void fullyLock() {  
  5.     putLock.lock();  
  6.     takeLock.lock();  
  7. }  
  8. /** 
  9.  * Unlocks to allow both puts and takes. 
  10.  */  
  11. void fullyUnlock() {  
  12.     takeLock.unlock();  
  13.     putLock.unlock();  
  14. }  

6.4、线程唤醒signal()
值得注意的是,对notEmpty和notFull的唤醒操作均使用的是signal()而不是signalAll()。
signalAll() 虽然能唤醒Condition上所有等待的线程,但却并不见得会节省资源,相反,唤醒操作会带来上下文切换,且会有锁的竞争。此外,由于此处获取的锁均是同一个(putLock或takeLock),同一时刻被锁的线程只有一个,也就无从谈起唤醒多个线程了。

6.5、LinkedBlockingQueue与ArrayBlockingQueue简要比较
ArrayBlockingQueue底层基于数组,创建时必须指定队列大小,“有界”;LinkedBlockingQueue“无界”,节点动态创建,节点出队后可被GC,故伸缩性较好;
ArrayBlockingQueue入队和出队使用同一个lock(但数据读写操作已非常简洁),读取和写入操作无法并行,LinkedBlockingQueue使用双锁可并行读写,其吞吐量更高。
ArrayBlockingQueue在插入或删除元素时直接放入数组指定位置(putIndex、takeIndex),不会产生或销毁任何额外的对象实例;而LinkedBlockingQueue则会生成一个额外的Node对象,在高效并发处理大量数据时,对GC的影响存在一定的区别。

参考、感谢:

http://blog.csdn.net/u010887744/article/details/73010691







你可能感兴趣的:(网络与多线程)