JDK源码分析—— ArrayBlockingQueue 和 LinkedBlockingQueue

目的:本文通过分析JDK源码来对比ArrayBlockingQueue 和LinkedBlockingQueue,以便日后灵活使用。

1. 在Java的Concurrent包中,添加了阻塞队列BlockingQueue,用于多线程编程。BlockingQueue的核心方法有:

boolean add(E e) ,把 e 添加到BlockingQueue里。如果BlockingQueue可以容纳,则返回true,否则抛出异常。
boolean offer(E e),表示如果可能的话,将 e 加到BlockingQueue里,即如果BlockingQueue可以容纳,则返回true,否则返回false。
void put(E e),把 e 添加到BlockingQueue里,如果BlockQueue没有空间,则调用此方法的线程被阻塞直到BlockingQueue里面有空间再继续。
E poll(long timeout, TimeUnit unit) ,取走BlockingQueue里排在首位的对象,若不能立即取出,则可以等time参数规定的时间,取不到时返回null。
E take() ,取走BlockingQueue里排在首位的对象,若BlockingQueue为空,则调用此方法的线程被阻塞直到BlockingQueue有新的数据被加入。
int drainTo(Collection<? super E> c) 和 int drainTo(Collection<?
super E> c, int maxElements)
,一次性从BlockingQueue获取所有可用的数据对象(还可以指定获取 数据的个数),通过该方法,可以提升获取数据效率,不需要多次分批加锁或
释放锁。

注意:BlockingQueue 不接受null 元素。试图add、put 或offer 一个null 元素时,某些实现会抛出NullPointerException。null 被用作指示poll 操作失败的警戒值。

2. BlockingQueue常用的四个实现类

ArrayBlockingQueue:规定大小的BlockingQueue,其构造函数必须带一个int参数来指明其大小.其所含的对象是以FIFO(先入先出)顺序排序的.
2)
LinkedBlockingQueue:大小不定的BlockingQueue,若其构造函数带一个规定大小的参数,生成的BlockingQueue
有大小限制,若不带大小参数,所生成的BlockingQueue的大小由Integer.MAX_VALUE来决定.其所含的对象是以FIFO(先入先
出)顺序排序的
3) PriorityBlockingQueue:类似于LinkedBlockQueue,但其所含对象的排序不是FIFO,而是依据对象的自然排序顺序或者是构造函数的Comparator决定的顺序.
4) SynchronousQueue:特殊的BlockingQueue,对其的操作必须是放和取交替完成的.

本文将从JDK源码层面分析对比ArrayBlockingQueue和LinkedBlockingQueue

3. ArrayBlockingQueue源码分析

ArrayBlockingQueue是一个由数组支持的有界阻塞队列。此队列按 FIFO(先进先出)原则对元素进行排序。队列的头部
是在队列中存在时间最长的元素,队列的尾部 是在队列中存在时间最短的元素。新元素插入到队列的尾部,队列检索操作则是从队列头部开始获得元素。
     这是一个典型的“有界缓存区”,固定大小的数组在其中保持生产者插入的元素和使用者提取的元素。一旦创建了这样的缓存区,就不能再增加其容量。试图向已满队列中放入元素会导致放入操作受阻塞;试图从空队列中检索元素将导致类似阻塞。

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

  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类中定义的变量有:

  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. * Concurrency control uses the classic two-condition algorithm
  11. * found in any textbook.
  12. */
  13. /** Main lock guarding all access */
  14. private final ReentrantLock lock;
  15. /** Condition for waiting takes */
  16. private final Condition notEmpty;
  17. /** Condition for waiting puts */
  18. 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()方法来激活该线程。激活之后,继续下面的插入操
作。

  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 == null) throw 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) 方法如下:

  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. * Circularly increment i.
  16. */
  17. final int inc(int i) {
  18. //此处可以看到使用了循环队列
  19. return (++i == items.length)? 0 : i;
  20. }

take()方法代码如下。take操作和put操作相反,故不作详细介绍。

  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() 方法如下:

  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无法并行执行!
4. LinkedBlockingQueue 源码分析

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

LinkedBlockingQueue 类中定义的变量有:

  1. /** The capacity bound, or Integer.MAX_VALUE if none */
  2. private final int capacity;
  3. /** Current number of elements */
  4. private final AtomicInteger count = new AtomicInteger(0);
  5. /** Head of linked list */
  6. private transient Node<E> head;
  7. /** Tail of linked list */
  8. private transient Node<E> last;
  9. /** Lock held by take, poll, etc */
  10. private final ReentrantLock takeLock = new ReentrantLock();
  11. /** Wait queue for waiting takes */
  12. private final Condition notEmpty = takeLock.newCondition();
  13. /** Lock held by put, offer, etc */
  14. private final ReentrantLock putLock = new ReentrantLock();
  15. /** Wait queue for waiting puts */
  16. private final Condition notFull = putLock.newCondition();

该类中定义了两个ReentrantLock锁:putLock和takeLock,分别用于put端和take端。也就是说,生成端和消费端各自独立拥有一把锁,避免了读(take)写(put)时互相竞争锁的情况。

  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 == null) throw 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)方法如下:

  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操作相反,故不作详细介绍。

  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()方法如下:

  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操作各有一把锁,可并行读取。

参考地址:

1). Java多线程-工具篇-BlockingQueuehttp://blog.csdn.net/xiaoliang_xie/article/details/6887115

2). Java多线程(五)之BlockingQueue深入分析http://blog.csdn.net/vernonzheng/article/details/8247564

JDK源码分析—— ArrayBlockingQueue 和 LinkedBlockingQueue的更多相关文章

  1. JDK 源码分析(4)—— HashMap/LinkedHashMap/Hashtable

    JDK 源码分析(4)-- HashMap/LinkedHashMap/Hashtable HashMap HashMap采用的是哈希算法+链表冲突解决,table的大小永远为2次幂,因为在初始化的时 ...

  2. JDK源码分析(三)—— LinkedList

    参考文档 JDK源码分析(4)之 LinkedList 相关

  3. JDK源码分析(一)—— String

    dir 参考文档 JDK源码分析(1)之 String 相关

  4. JDK源码分析(2)LinkedList

    JDK版本 LinkedList简介 LinkedList 是一个继承于AbstractSequentialList的双向链表.它也可以被当作堆栈.队列或双端队列进行操作. LinkedList 实现 ...

  5. 【JDK】JDK源码分析-LinkedHashMap

    概述 前文「JDK源码分析-HashMap(1)」分析了 HashMap 主要方法的实现原理(其他问题以后分析),本文分析下 LinkedHashMap. 先看一下 LinkedHashMap 的类继 ...

  6. 【JDK】JDK源码分析-HashMap(1)

    概述 HashMap 是 Java 开发中最常用的容器类之一,也是面试的常客.它其实就是前文「数据结构与算法笔记(二)」中「散列表」的实现,处理散列冲突用的是“链表法”,并且在 JDK 1.8 做了优 ...

  7. 【JDK】JDK源码分析-TreeMap(2)

    前文「JDK源码分析-TreeMap(1)」分析了 TreeMap 的一些方法,本文分析其中的增删方法.这也是红黑树插入和删除节点的操作,由于相对复杂,因此单独进行分析. 插入操作 该操作其实就是红黑 ...

  8. 【JDK】JDK源码分析-Vector

    概述 上文「JDK源码分析-ArrayList」主要分析了 ArrayList 的实现原理.本文分析 List 接口的另一个实现类:Vector. Vector 的内部实现与 ArrayList 类似 ...

  9. 【JDK】JDK源码分析-ArrayList

    概述 ArrayList 是 List 接口的一个实现类,也是 Java 中最常用的容器实现类之一,可以把它理解为「可变数组」. 我们知道,Java 中的数组初始化时需要指定长度,而且指定后不能改变. ...

随机推荐

  1. Kendo UI开发教程(22): Kendo MVVM 数据绑定(十一) Value

    Value绑定可以把ViewModel的某个属性绑定到DOM元素或某个UI组件的Value属性.当用户修改DOM元素或UI组件的值时,绑定的ViewModel的值也随之发生改名.同样,如果ViewMo ...

  2. 在github 网页上,删除已经建好的库

    在github 上面怎么删除已经建好的库 点击你要删除的库,然后找到Setting 找到如图所示的Delete 在输入框里面输入你要删除的库的名字 最后点击按钮,就可以删掉了

  3. 好多NFS的文章

    http://www.cnblogs.com/lidabo/category/587288.html http://www.cnblogs.com/lidabo/p/4380555.html

  4. [置顶] JSP分页,使用Hibernate+mysql

    此代码为博主参考巴巴运动网源码所得,大部分一样,略有修改,在这里分享给大家,也方便自己以后写代码直接copy,看网上很多分页代码JSP里是用JAVA代码,博主原来也是这样,看到源码了解了JSTL,建议 ...

  5. (step6.3.3)hdu 1150(Machine Schedule——二分图的最小点覆盖数)

    题目大意:第一行输入3个整数n,m,k.分别表示女生数(A机器数),男生数(B机器数),以及它们之间可能的组合(任务数). 在接下来的k行中,每行有3个整数c,a,b.表示任务c可以有机器A的a状态或 ...

  6. Yarn的ApplicationMaster管理

    首先client向ResourceManager提交程序(包括ApplicationMaster程序,ApplicationMaster启动命令,用户程序)后,ResourceManager向资源调度 ...

  7. Android API中被忽略的几个函数接口

    1. MotionEvent的几个函数 下面的方法都支持多点触摸,即可以对单个触摸点调用下面的方法 1.1 getPressure() 这个api 可以获取到手指触摸屏幕时候的压力,但是需要硬件和驱动 ...

  8. MFC程序的消息处理顺序

    MFC应用程序中处理消息的顺序 1.AfxWndProc()      该函数负责接收消息,找到消息所属的CWnd对象,然后调用AfxCallWndProc 2.AfxCallWndProc()  该 ...

  9. 判断Webbrowser是否加载完成

    var Form1: TForm1; glpDisp: IDispatch = nil; implementation {$R *.dfm} procedure TForm1.EmbeddedWB1D ...

  10. oracle系统包——dbms_random用法及order by 小结(转)

    dbms_random是一个可以生成随机数值或者字符串的程序包. 这个包有initialize().seed().terminate().value().normal().random().strin ...