BlockingQueue接口及实现类分析

来源:互联网 发布:数据库配置文件怎么写 编辑:程序博客网 时间:2024/05/29 12:52

1 BlockingQueue 接口及其实现类


BlocingQueue接口定义如下,仅列举几个常用方法:

  • put(E)  在队列尾部放入元素,若队列满则等待;
  • take()  取队列头部元素返回,若队列空则等待;
  • offer(E)  在队列尾部放入元素,若成功则返回true, 否则false;不阻塞;
  • poll()      取队列头部元素返回,若有元素则返回元素,否则null;不阻塞;

实现BlockingQueue的常用接口:

  • ArrayBlockingQueue: 使用循环数组实现队列,由一把锁控制put 和 take, 同一把锁上实现了两个等待队列(Condition);
  • LinkedBlockingQueue: 使用链表实现队列,由两把锁分别控制put 和 take,两个等待队列分别在两把锁上;
  • PriorityBlockingQueue: 使用小顶堆实现优先级队列,取出元素顺序有Comparable, Comparator决定,多线程安全;


2 ArrayBlockingQueue 源码解读

ArrayBlockingQueue创建的时候需要指定容量capacity(可以存储的最大的元素个数,因为它不会自动扩容)。其中一个构造方法为:

[java] view plain copy print?在CODE上查看代码片派生到我的代码片
  1. public ArrayBlockingQueue(int capacity, boolean fair) {  
  2.        if (capacity <= 0)  
  3.            throw new IllegalArgumentException();  
  4.        this.items = (E[]) new Object[capacity];  
  5.        lock = new ReentrantLock(fair);  
  6.        notEmpty = lock.newCondition();  
  7.        notFull =  lock.newCondition();  
  8.    }  

    ArrayBlockingQueue类中定义的变量有:

[java] view plain copy print?在CODE上查看代码片派生到我的代码片
  1. /** The queued items  */  
  2. private final E[] items;  
  3. /** items index for next take, poll or remove */  
  4. private int takeIndex;  
  5. /** items index for next put, offer, or add. */  
  6. private int putIndex;  
  7. /** Number of items in the queue */  
  8. private int count;  
  9.   
  10. /* 
  11.  * Concurrency control uses the classic two-condition algorithm 
  12.  * found in any textbook. 
  13.  */  
  14.   
  15. /** Main lock guarding all access */  
  16. private final ReentrantLock lock;  
  17. /** Condition for waiting takes */  
  18. private final Condition notEmpty;  
  19. /** Condition for waiting puts */  
  20. private final Condition notFull;  
使用数组items来存储元素,由于是循环队列,使用takeIndex和putIndex来标记put和take的位置。可以看到,该类中只定义了一个锁ReentrantLock,定义两个Condition对象:notEmputy和notFull,分别用来对take和put操作进行所控制。注:本文主要讲解put()和take()操作,其他方法类似。

put(E e)方法的源码如下。进行put操作之前,必须获得锁并进行加锁操作,以保证线程安全性。加锁后,若发现队列已满,则调用notFull.await()方法,如当前线程陷入等待。直到其他线程take走某个元素后,会调用notFull.signal()方法来激活该线程。激活之后,继续下面的插入操作。

[java] view plain copy print?在CODE上查看代码片派生到我的代码片
  1. /** 
  2.      * Inserts the specified element at the tail of this queue, waiting 
  3.      * for space to become available if the queue is full. 
  4.      * 
  5.      */  
  6.     public void put(E e) throws InterruptedException {  
  7.         //不能存放 null  元素  
  8.         if (e == nullthrow new NullPointerException();  
  9.         final E[] items = this.items;   //数组队列  
  10.         final ReentrantLock lock = this.lock;  
  11.         //加锁  
  12.         lock.lockInterruptibly();  
  13.         try {  
  14.             try {  
  15.                 //当队列满时,调用notFull.await()方法,使该线程阻塞。  
  16.                 //直到take掉某个元素后,调用notFull.signal()方法激活该线程。  
  17.                 while (count == items.length)  
  18.                     notFull.await();  
  19.             } catch (InterruptedException ie) {  
  20.                 notFull.signal(); // propagate to non-interrupted thread  
  21.                 throw ie;  
  22.             }  
  23.             //把元素 e 插入到队尾  
  24.             insert(e);  
  25.         } finally {  
  26.             //解锁  
  27.             lock.unlock();  
  28.         }  
  29.     }  
insert(E e) 方法如下:
[java] view plain copy print?在CODE上查看代码片派生到我的代码片
  1.   /** 
  2.    * Inserts element at current put position, advances, and signals. 
  3.    * Call only when holding lock. 
  4.    */  
  5.   private void insert(E x) {  
  6.       items[putIndex] = x;    
  7. //下标加1或者等于0  
  8.       putIndex = inc(putIndex);  
  9.       ++count;  //计数加1  
  10. //若有take()线程陷入阻塞,则该操作激活take()线程,继续进行取元素操作。  
  11. //若没有take()线程陷入阻塞,则该操作无意义。  
  12.       notEmpty.signal();  
  13.   }  
  14.   
  15. **  
  16.    * Circularly increment i.  
  17.    */  
  18.   final int inc(int i) {  
  19. //此处可以看到使用了循环队列  
  20.       return (++i == items.length)? 0 : i;  
  21.   }  
take()方法代码如下。take操作和put操作相反,故不作详细介绍。
[java] view plain copy print?在CODE上查看代码片派生到我的代码片
  1. public E take() throws InterruptedException {  
  2.         final ReentrantLock lock = this.lock;  
  3.         lock.lockInterruptibly();  //加锁  
  4.         try {  
  5.             try {  
  6.                 //当队列空时,调用notEmpty.await()方法,使该线程阻塞。  
  7.                 //直到take掉某个元素后,调用notEmpty.signal()方法激活该线程。  
  8.                 while (count == 0)  
  9.                     notEmpty.await();  
  10.             } catch (InterruptedException ie) {  
  11.                 notEmpty.signal(); // propagate to non-interrupted thread  
  12.                 throw ie;  
  13.             }  
  14.             //取出队头元素  
  15.             E x = extract();  
  16.             return x;  
  17.         } finally {  
  18.             lock.unlock();  //解锁  
  19.         }  
  20.     }  
extract() 方法如下:
[java] view plain copy print?在CODE上查看代码片派生到我的代码片
  1. /** 
  2.      * Extracts element at current take position, advances, and signals. 
  3.      * Call only when holding lock. 
  4.      */  
  5.     private E extract() {  
  6.         final E[] items = this.items;  
  7.         E x = items[takeIndex];  
  8.         items[takeIndex] = null;  
  9.         takeIndex = inc(takeIndex);  
  10.         --count;  
  11.         notFull.signal();  
  12.         return x;  
  13.     }  
小结:进行put和take操作,共用同一个锁对象。也即是说,put和take无法并行执行!


3 LinkedBlockingQueue 源码解读

基于链表的阻塞队列,同ArrayListBlockingQueue类似,其内部也维持着一个数据缓冲队列(该队列由一个链表构成),当生产者往队列中放入一个数据时,队列会从生产者手中获取数据,并缓存在队列内部,而生产者立即返回;只有当队列缓冲区达到最大值缓存容量时(LinkedBlockingQueue可以通过构造函数指定该值),才会阻塞生产者队列,直到消费者从队列中消费掉一份数据,生产者线程会被唤醒,反之对于消费者这端的处理也基于同样的原理。而LinkedBlockingQueue之所以能够高效的处理并发数据,还因为其对于生产者端和消费者端分别采用了独立的锁来控制数据同步,这也意味着在高并发的情况下生产者和消费者可以并行地操作队列中的数据,以此来提高整个队列的并发性能。
作为开发者,我们需要注意的是,如果构造一个LinkedBlockingQueue对象,而没有指定其容量大小,LinkedBlockingQueue会默认一个类似无限大小的容量(Integer.MAX_VALUE),这样的话,如果生产者的速度一旦大于消费者的速度,也许还没有等到队列满阻塞产生,系统内存就有可能已被消耗殆尽了。

 LinkedBlockingQueue 类中定义的变量有:

[java] view plain copy print?在CODE上查看代码片派生到我的代码片
  1. /** The capacity bound, or Integer.MAX_VALUE if none */  
  2. private final int capacity;  
  3.   
  4. /** Current number of elements */  
  5. private final AtomicInteger count = new AtomicInteger(0);  
  6.   
  7. /** Head of linked list */  
  8. private transient Node<E> head;  
  9.   
  10. /** Tail of linked list */  
  11. private transient Node<E> last;  
  12.   
  13. /** Lock held by take, poll, etc */  
  14. private final ReentrantLock takeLock = new ReentrantLock();  
  15.   
  16. /** Wait queue for waiting takes */  
  17. private final Condition notEmpty = takeLock.newCondition();  
  18.   
  19. /** Lock held by put, offer, etc */  
  20. private final ReentrantLock putLock = new ReentrantLock();  
  21.   
  22. /** Wait queue for waiting puts */  
  23. private final Condition notFull = putLock.newCondition();  
该类中定义了两个ReentrantLock锁:putLock和takeLock,分别用于put端和take端。也就是说,生成端和消费端各自独立拥有一把锁,避免了读(take)写(put)时互相竞争锁的情况。
[java] view plain copy print?在CODE上查看代码片派生到我的代码片
  1. /** 
  2.      * Inserts the specified element at the tail of this queue, waiting if 
  3.      * necessary for space to become available. 
  4.      */  
  5.     public void put(E e) throws InterruptedException {  
  6.         if (e == nullthrow new NullPointerException();  
  7.         // Note: convention in all put/take/etc is to preset local var  
  8.         // holding count negative to indicate failure unless set.  
  9.         int c = -1;  
  10.         final ReentrantLock putLock = this.putLock;  
  11.         final AtomicInteger count = this.count;  
  12.         putLock.lockInterruptibly(); //加 putLock 锁  
  13.         try {  
  14.             /* 
  15.              * Note that count is used in wait guard even though it is 
  16.              * not protected by lock. This works because count can 
  17.              * only decrease at this point (all other puts are shut 
  18.              * out by lock), and we (or some other waiting put) are 
  19.              * signalled if it ever changes from 
  20.              * capacity. Similarly for all other uses of count in 
  21.              * other wait guards. 
  22.              */  
  23.             //当队列满时,调用notFull.await()方法释放锁,陷入等待状态。  
  24.             //有两种情况会激活该线程  
  25.             //第一、 某个put线程添加元素后,发现队列有空余,就调用notFull.signal()方法激活阻塞线程  
  26.             //第二、 take线程取元素时,发现队列已满。则其取出元素后,也会调用notFull.signal()方法激活阻塞线程  
  27.             while (count.get() == capacity) {   
  28.                     notFull.await();  
  29.             }  
  30.             // 把元素 e 添加到队列中(队尾)  
  31.             enqueue(e);  
  32.             c = count.getAndIncrement();  
  33.             //发现队列未满,调用notFull.signal()激活阻塞的put线程(可能存在)  
  34.             if (c + 1 < capacity)  
  35.                 notFull.signal();  
  36.         } finally {  
  37.             putLock.unlock();  
  38.         }  
  39.         if (c == 0)  
  40.             //队列空,说明已经有take线程陷入阻塞,故调用signalNotEmpty激活阻塞的take线程  
  41.             signalNotEmpty();  
  42.     }  
enqueue(E e)方法如下:
[java] view plain copy print?在CODE上查看代码片派生到我的代码片
  1. /** 
  2.  * Creates a node and links it at end of queue. 
  3.  * @param x the item 
  4.  */  
  5. private void enqueue(E x) {  
  6.     // assert putLock.isHeldByCurrentThread();  
  7.     last = last.next = new Node<E>(x);  
  8. }  
take()方法代码如下。take操作和put操作相反,故不作详细介绍。
[java] view plain copy print?在CODE上查看代码片派生到我的代码片
  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)  
  14.                notEmpty.signal();  
  15.        } finally {  
  16.            takeLock.unlock();  
  17.        }  
  18.        if (c == capacity)  
  19.            signalNotFull();  
  20.        return x;  
  21.    }  
dequeue()方法如下:
[java] view plain copy print?在CODE上查看代码片派生到我的代码片
  1. /** 
  2.  * Removes a node from head of queue. 
  3.  * @return the node 
  4.  */  
  5. private E dequeue() {  
  6.     // assert takeLock.isHeldByCurrentThread();  
  7.     Node<E> h = head;  
  8.     Node<E> first = h.next;  
  9.     h.next = h; // help GC  
  10.     head = first;  
  11.     E x = first.item;  
  12.     first.item = null;  
  13.     return x;  
  14. }  

小结:take和put操作各有一把锁,可并行读取。


4 SynchronousQueue, ArrayBlockingQueue 和 LinkedBlockingQueue 性能对比

  • 线程多(>20),Queue长度长(>30),使用LinkedBlockingQueue

  • 线程少 (<20) ,Queue长度短 (<30) , 使用SynchronousQueue

当然,使用SynchronousQueue的时候不要忘记应用的扩展,如果将来需要进行扩展还是选择LinkedBlockingQueue好,尽量把SynchronousQueue限制在特殊场景中使用。

  • 少用ArrayBlcokingQueue,似乎没找到它的好处,高手给给建议吧!

参考文章:

1 JDK源码分析—— ArrayBlockingQueue 和 LinkedBlockingQueue

2 SynchronousQueue、LinkedBlockingQueue、ArrayBlockingQueue性能测试


0 0
原创粉丝点击