上一篇我们已经学习过了 ArrayBlockingQueue的知识及相关方法的使用,这一篇我们就来再学习一下ArrayBlockingQueue的亲戚 LinkedBlockingQueue。在集合类中 ArrayList与 LinkedList会常常拿来比较,ArrayList内部实现是基于数组的,而 LinkedList内部实现是基于链表,所以他们之间会有很多不同,但是本文不会去重点讨论,感兴趣的朋友可以参考我之前发过的几篇文章,那么有请本节的主角 LinkedBlockingQueue!

LinkedBlockingQueue

LinkedBlockingQueue是一个一个基于已链接节点的、范围任意(相对而论)的 blocking queue。此队列按 FIFO(先进先出)排序元素。队列的头部 是在队列中时间最长的元素。队列的尾部 是在队列中时间最短的元素。新元素插入到队列的尾部,并且队列获取操作会获得位于队列头部的元素。链接队列的吞吐量通常要高于基于数组的队列,但是在大多数并发应用程序中,其可预知的性能要低。

可选的容量范围构造方法参数作为防止队列过度扩展的一种方法。如果未指定容量,则它等于 Integer.MAX_VALUE。除非插入节点会使队列超出容量,否则每次插入后会动态地创建链接节点。

LinkedBlockingQueue及其迭代器实现了 Collection 和 Iterator 接口的所有可选 方法。

我们已经学习过了 ArrayBlockingQueue,所以学习 LinkedBlockingQueue就自然比较轻松,所以本文对于已经明确的相关概念就不做过多介绍了,而是重点放在两者的区别之上。

1.成员变量

与ArrayBlockingQueue不同 LinkedBlockingQueue的成员变量有些变化,以下是 LinkedBlockingQueue的成员变量:

  1. /** 容量范围,默认值为 Integer.MAX_VALUE */
  2. private final int capacity;
  3. /** 当前队列中元素数量 */
  4. private final AtomicInteger count = new AtomicInteger(0);
  5. /** 头节点 */
  6. private transient Node<E> head;
  7. /** 尾节点 */
  8. private transient Node<E> last;
  9. /** take, poll等方法的锁 */
  10. private final ReentrantLock takeLock = new ReentrantLock();
  11. /** 获取队列的 Condition(条件)实例 */
  12. private final Condition notEmpty = takeLock.newCondition();
  13. /** put, offer等方法的锁 */
  14. private final ReentrantLock putLock = new ReentrantLock();
  15. /** 插入队列的 Condition(条件)实例 */
  16. private final Condition notFull = putLock.newCondition();

1)首先 LinkedBlockingQueue明确了容量变量,当为指定容量时,默认容量为Int的最大值Integer.MAX_VALUE。

2)队列元素数量变量 count采用的是 AtomicInteger ,而不是普通的Int型。CAS相关可参考http://286.iteye.com/blog/2295165

3)LinkedBlockingQueue内部队列实现使用的是 Node节点类,这与 LinkedList类似。

4)最后也是最重要的一点,那就是获取与插入操作分成了两个锁:takeLock与 putLock来处理,这点下面还会重点分析。

2.构造方法

有三个构造方法,分别为默认,指定容量,指定容量和初始元素。

  1. /**
  2. * 创建一个容量为 Integer.MAX_VALUE 的 LinkedBlockingQueue
  3. */
  4. public LinkedBlockingQueue() {
  5. this(Integer.MAX_VALUE);
  6. }
  7. /**
  8. * 创建一个具有给定(固定)容量的 LinkedBlockingQueue
  9. */
  10. public LinkedBlockingQueue(int capacity) {
  11. if (capacity <= 0)
  12. throw new IllegalArgumentException();
  13. this.capacity = capacity;
  14. last = head = new Node<E>(null);
  15. }
  16. /**
  17. * 创建一个容量为 Integer.MAX_VALUE 的 LinkedBlockingQueue,
  18. * 最初包含给定 collection 的元素,元素按该 collection 迭代器的遍历顺序添加。
  19. */
  20. public LinkedBlockingQueue(Collection<? extends E> c) {
  21. this(Integer.MAX_VALUE);
  22. for (E e : c)
  23. add(e);
  24. }

默认构造方法创建一个容量为 Integer.MAX_VALUE的 LinkedBlockingQueue实例。

第二种构造方法,指定了队列容量,首先判断指定容量是否大于零,否则抛出异常。然后为 capacity 赋值,最后创建空节点,并指向 head与 last,两者的 item与 next此时均为 null。

最后一种,利用循环向队列中添加指定集合中的元素。

3.Node类

LinkedBlockingQueue内部列表实现是使用的 Node内部类,Node类也并不复杂,以下是其源代码:

  1. /**
  2. * 节点类
  3. */
  4. static class Node<E> {
  5. /** volatile保障读写分离 */
  6. volatile E item;
  7. Node<E> next;
  8. Node(E x) {
  9. item = x;
  10. }
  11. }

item用于表示元素对象,next指向链表的下一个节点。

LinkedBlockingQueue的大部分方法其实是与  ArrayBlockingQueue类似的,所以本文就只介绍不同于ArrayBlockingQueue的相关方法。

4.添加元素

1)add方法

add方法相同就不介绍了,同样调用的是offer方法。

2)offer方法

将指定元素插入到此队列的尾部(如果立即可行且不会超出此队列的容量),在成功时返回 true,如果此队列已满,则返回 false。当使用有容量限制的队列时,此方法通常要优于 add 方法,后者可能无法插入元素,而只是抛出一个异常。 

与ArrayBlockingQueue不同,LinkedBlockingQueue多了一些容量方面的判断。

  1. /**
  2. * 将指定元素插入到此队列的尾部(如果立即可行且不会超出此队列的容量)
  3. * 在成功时返回 true,如果此队列已满,则返回 false。
  4. * 当使用有容量限制的队列时,此方法通常要优于 add 方法,
  5. * 后者可能无法插入元素,而只是抛出一个异常。
  6. */
  7. public boolean offer(E e) {
  8. //判断添加元素是否为null
  9. if (e == null)
  10. throw new NullPointerException();
  11. //第一点不同,使用原子类操作count,因为有两个锁
  12. final AtomicInteger count = this.count;
  13. //判断容量,队列是否已满
  14. if (count.get() == capacity)
  15. return false;
  16. int c = -1;
  17. final ReentrantLock putLock = this.putLock;
  18. //获取添加锁
  19. putLock.lock();
  20. try {
  21. //再次判断,如果队列未满
  22. if (count.get() < capacity) {
  23. //插入元素
  24. insert(e);
  25. //增加元素数count
  26. c = count.getAndIncrement();
  27. if (c + 1 < capacity)
  28. //未满则唤醒添加线程
  29. notFull.signal();
  30. }
  31. } finally {
  32. //释放锁
  33. putLock.unlock();
  34. }
  35. //c等于0说明添加成功
  36. if (c == 0)
  37. //唤醒读取线程
  38. signalNotEmpty();
  39. return c >= 0;
  40. }

可以看到offer方法的关键在于 insert方法。

3)insert方法

insert方法非常简单,但是却不要小看。

  1. /**
  2. * 再队尾添加元素
  3. */
  4. private void insert(E x) {
  5. last = last.next = new Node<E>(x);
  6. }

首先,根据指定参数x创建一个Node实例。

然后,将原尾节点的next指向此节点。

最后,将尾节点设置尾此节点。

这样新添加的节点就成为了新的尾节点。

当向链表中添加第一个节点时,因为在初始化时

  1. last = head = new Node<E>(null);

所以此时 head与 last指向的是同一个对象new Node<E>(null)。

之后将last.next指向x。

  1. last.next = new Node<E>(x);

因为此时 head与 last是同一个对象,所以 head.next也指向x。

最后将 last指向x。

  1. last =  new Node<E>(x);

这样 head的next就指向了 last。此时head中的 item仍为 null。

4)put方法

将指定元素插入到此队列的尾部,如有必要,则等待空间变得可用。

  1. /**
  2. * 将指定元素插入到此队列的尾部,如有必要,则等待空间变得可用
  3. */
  4. public void put(E e) throws InterruptedException {
  5. //判断添加元素是否为null
  6. if (e == null)
  7. throw new NullPointerException();
  8. int c = -1;
  9. final ReentrantLock putLock = this.putLock;
  10. final AtomicInteger count = this.count;
  11. //获取插入的可中断锁
  12. putLock.lockInterruptibly();
  13. try {
  14. try {
  15. //判断队列是否已满
  16. while (count.get() == capacity)
  17. //如果已满则阻塞添加线程
  18. notFull.await();
  19. } catch (InterruptedException ie) {
  20. //失败就唤醒添加线程
  21. notFull.signal();
  22. throw ie;
  23. }
  24. //添加元素
  25. insert(e);
  26. //修改c值
  27. c = count.getAndIncrement();
  28. //根据c值判断队列是否已满
  29. if (c + 1 < capacity)
  30. //未满则唤醒添加线程
  31. notFull.signal();
  32. } finally {
  33. //释放锁
  34. putLock.unlock();
  35. }
  36. //c等于0代表添加成功
  37. if (c == 0)
  38. signalNotEmpty();
  39. }

5.获取元素

1)peek方法

peek方法获取但不移除此队列的头;如果此队列为空,则返回 null。

  1. /**
  2. * 获取但不移除此队列的头;如果此队列为空,则返回 null
  3. */
  4. public E peek() {
  5. //判断元素数是否为0
  6. if (count.get() == 0)
  7. return null;
  8. final ReentrantLock takeLock = this.takeLock;
  9. //获取获取锁
  10. takeLock.lock();
  11. try {
  12. //头节点的 next节点即为添加的第一个节点
  13. Node<E> first = head.next;
  14. //如果不为空则返回该节点
  15. if (first == null)
  16. return null;
  17. else
  18. return first.item;
  19. } finally {
  20. //释放锁
  21. takeLock.unlock();
  22. }
  23. }

peek方法从头节点直接就可以获取到第一个添加的元素,所以效率是比较高的。如果不存在则返回null。

2)poll方法

poll方法获取并移除此队列的头,如果此队列为空,则返回 null。

  1. /**
  2. * 获取并移除此队列的头,如果此队列为空,则返回 null
  3. */
  4. public E poll() {
  5. final AtomicInteger count = this.count;
  6. //判断元素数量
  7. if (count.get() == 0)
  8. return null;
  9. E x = null;
  10. int c = -1;
  11. final ReentrantLock takeLock = this.takeLock;
  12. //获取获取锁
  13. takeLock.lock();
  14. try {
  15. //再次判断元素数量
  16. if (count.get() > 0) {
  17. //调用extract方法获取第一个元素
  18. x = extract();
  19. //c=count++
  20. c = count.getAndDecrement();
  21. //如果队列中含有元素
  22. if (c > 1)
  23. //唤醒读取线程
  24. notEmpty.signal();
  25. }
  26. } finally {
  27. //释放锁
  28. takeLock.unlock();
  29. }
  30. //如果队列已满
  31. if (c == capacity)
  32. //唤醒等待中的添加线程
  33. signalNotFull();
  34. return x;
  35. }

poll与 peek方法不同在于poll获取完元素后移除这个元素,获取与移除是通过 extract()方法实现的。

注意:其中需要注意的是最后部分代码:

  1. //如果队列已满
  2. if (c == capacity)
  3. //唤醒等待中的添加线程
  4. signalNotFull();

肯定会有朋友有以下疑问:

1)队列都已经满了,还需要唤醒添加线程干什么?

2)线程满了就不应该再向里面添加元素了啊?

3)signalNotFull方法是干什么的?

signalNotFull方法的作用是唤醒等待中的put线程,signalNotFull只能被 take/poll方法调用,以下是 signalNotFull方法的源代码:

  1. /**
  2. * 唤醒等待中的put线程,只能被 take/poll方法调用
  3. */
  4. private void signalNotFull() {
  5. final ReentrantLock putLock = this.putLock;
  6. //获取锁
  7. putLock.lock();
  8. try {
  9. //唤醒添加线程
  10. notFull.signal();
  11. } finally {
  12. //释放锁
  13. putLock.unlock();
  14. }
  15. }

前两点问题其实转换一下角度就能很好的理解了,虽然队列已经满了,但是此时本线程已经完成了添加,但是其他线程还在等待获取条件进行添加,如果不去主动唤醒的话,那么这些添加操作就只能无限期的等待下去,所以这些等待的添加操作就会失效。所以此时需要唤醒已经排队的添加线程,虽然他们已经无法添加元素至队列。

3)extract方法

extract方法用于获取并移除头节点。

  1. /**
  2. * 获取并移除头节点
  3. */
  4. private E extract() {
  5. //获取第一个节点,即 head的下一个元素
  6. Node<E> first = head.next;
  7. //将head指向此元素
  8. head = first;
  9. //获取元素值
  10. E x = first.item;
  11. //清除first的item元素为空,即head元素的item为空
  12. first.item = null;
  13. //返回
  14. return x;
  15. }

这里需要注意的是这里指的头节点并不是 head,而是 head的 next所指 Node的 item元素。因为 head的 item永远为 null。last的 next永远为 null。

4)take方法

获取并移除此队列的头部,在元素变得可用之前一直等待(如果有必要)。

  1. /**
  2. * 获取并移除此队列的头部,在元素变得可用之前一直等待(如果有必要)
  3. */
  4. public E take() throws InterruptedException {
  5. E x;
  6. int c = -1;
  7. final AtomicInteger count = this.count;
  8. final ReentrantLock takeLock = this.takeLock;
  9. //获取可中断锁
  10. takeLock.lockInterruptibly();
  11. try {
  12. try {
  13. //判断队列是否含有元素
  14. while (count.get() == 0)
  15. //没有元素就阻塞获取线程,因为没有元素所以获取线程也就没有必要运行
  16. notEmpty.await();
  17. } catch (InterruptedException ie) {
  18. //失败就唤醒获取线程
  19. notEmpty.signal();
  20. throw ie;
  21. }
  22. //调用 extract方法获取元素
  23. x = extract();
  24. //计数c的新值
  25. c = count.getAndDecrement();
  26. //如果元素数大于1
  27. if (c > 1)
  28. //唤醒获取线程
  29. notEmpty.signal();
  30. } finally {
  31. //释放锁
  32. takeLock.unlock();
  33. }
  34. //如果队列已满
  35. if (c == capacity)
  36. //唤醒还在等待的put线程
  37. signalNotFull();
  38. return x;
  39. }

与 poll方法类似,只是take方法采用阻塞的方式来获取元素。

7.其他方法

1)remainingCapacity方法

  1. /**
  2. * 返回理想情况下(没有内存和资源约束)此队列可接受并且不会被阻塞的附加元素数量
  3. */
  4. public int remainingCapacity() {
  5. return capacity - count.get();
  6. }

也就是返回可以立即添加元素的数量。

2)iterator方法

iterator方法返回在队列中的元素上按适当顺序进行迭代的迭代器。返回的 Iterator 是一个“弱一致”的迭代器,从不抛出 ConcurrentModificationException,并且确保可遍历迭代器构造后所存在的所有元素,并且可能(但并不保证)反映构造后的所有修改。

  1. /**
  2. * 返回Itr实例
  3. */
  4. public Iterator<E> iterator() {
  5. return new Itr();
  6. }

iterator方法返回的是一个Itr内部类的实例,通过这个实例可以遍历整个队列。以下是Itr内部类的源代码:

  1. private class Itr implements Iterator<E> {
  2. //当前节点
  3. private Node<E> current;
  4. private Node<E> lastRet;
  5. //当前元素
  6. private E currentElement;
  7. Itr() {
  8. final ReentrantLock putLock = LinkedBlockingQueue.this.putLock;
  9. final ReentrantLock takeLock = LinkedBlockingQueue.this.takeLock;
  10. //获取获取与添加锁
  11. putLock.lock();
  12. takeLock.lock();
  13. try {
  14. current = head.next;
  15. if (current != null)
  16. currentElement = current.item;
  17. } finally {
  18. takeLock.unlock();
  19. putLock.unlock();
  20. }
  21. }
  22. public boolean hasNext() {
  23. return current != null;
  24. }
  25. public E next() {
  26. final ReentrantLock putLock = LinkedBlockingQueue.this.putLock;
  27. final ReentrantLock takeLock = LinkedBlockingQueue.this.takeLock;
  28. putLock.lock();
  29. takeLock.lock();
  30. try {
  31. if (current == null)
  32. throw new NoSuchElementException();
  33. E x = currentElement;
  34. lastRet = current;
  35. current = current.next;
  36. if (current != null)
  37. currentElement = current.item;
  38. return x;
  39. } finally {
  40. takeLock.unlock();
  41. putLock.unlock();
  42. }
  43. }
  44. public void remove() {
  45. if (lastRet == null)
  46. throw new IllegalStateException();
  47. final ReentrantLock putLock = LinkedBlockingQueue.this.putLock;
  48. final ReentrantLock takeLock = LinkedBlockingQueue.this.takeLock;
  49. putLock.lock();
  50. takeLock.lock();
  51. try {
  52. Node<E> node = lastRet;
  53. lastRet = null;
  54. Node<E> trail = head;
  55. Node<E> p = head.next;
  56. while (p != null && p != node) {
  57. trail = p;
  58. p = p.next;
  59. }
  60. if (p == node) {
  61. p.item = null;
  62. trail.next = p.next;
  63. if (last == p)
  64. last = trail;
  65. int c = count.getAndDecrement();
  66. if (c == capacity)
  67. notFull.signalAll();
  68. }
  69. } finally {
  70. takeLock.unlock();
  71. putLock.unlock();
  72. }
  73. }
  74. }

Itr类不复杂,我就不详细解释了。

3)清除方法

clear,drainTo等方法与 ArrayBlockingQueue类似,这里就不说了。

8,.LinkedBlockingQueue与 ArrayBlockingQueue

        1)内部实现不同

ArrayBlockingQueue内部队列存储使用的是数组:

  1. private final E[] items;

而 LinkedBlockingQueue内部队列存储使用的是Node节点内部类:

  1. static class Node<E> {
  2. /** The item, volatile to ensure barrier separating write and read */
  3. volatile E item;
  4. Node<E> next;
  5. Node(E x) { item = x; }
  6. }

        2)队列中锁的实现不同

  1. /** LinkedBlockingQueue的获取锁 */
  2. private final ReentrantLock takeLock = new ReentrantLock();
  3. /** LinkedBlockingQueue的添加锁 */
  4. private final ReentrantLock putLock = new ReentrantLock();
  5. /** ArrayBlockingQueue的唯一锁 */
  6. private final ReentrantLock lock;

从源代码就可以看出 ArrayBlockingQueue实现的队列中的锁是没有分离的,即添加与获取使用的是同一个锁;而 LinkedBlockingQueue实现的队列中的锁是分离的,即添加用的是 putLock,获取是 takeLock。

 3)初始化条件不同

ArrayBlockingQueue实现的队列中必须指定队列的大小。

LinkedBlockingQueue实现的队列中可以不指定队列的大小,默认容量为Integer.MAX_VALUE。

4)操作不同

ArrayBlockingQueue无论是添加还是获取使用的是同一个锁,所以添加的同时就不能读取,读取的同时就不能添加,所以锁方面性能不如 LinkedBlockingQueue。

LinkedBlockingQueue读取与添加操作使用不同的锁,因为其内部实现的特殊性,添加的时候只需要修改 last即可,而不会影响 head节点。而获取时也只需要修改 head节点即可,同样不会影响 last节点。所以在添加获取方面理论上性能会高于 ArrayBlockingQueue。

所以 LinkedBlockingQueue更适合实现生产者-消费者队列。

Java并发之LinkedBlockingQueue的更多相关文章

  1. Java并发之BlockingQueue的使用

    Java并发之BlockingQueue的使用 一.简介 前段时间看到有些朋友在网上发了一道面试题,题目的大意就是:有两个线程A,B,  A线程每200ms就生成一个[0,100]之间的随机数, B线 ...

  2. 深入理解Java并发之synchronized实现原理

    深入理解Java类型信息(Class对象)与反射机制 深入理解Java枚举类型(enum) 深入理解Java注解类型(@Annotation) 深入理解Java类加载器(ClassLoader) 深入 ...

  3. Java并发之Semaphore的使用

    Java并发之Semaphore的使用 一.简介 今天突然发现,看着自己喜欢的球队发挥如此的棒,然后写着博客,这种感觉很爽.现在是半场时间,就趁着这个时间的空隙,说说Java并发包中另外一个重量级的类 ...

  4. JAVA并发之阻塞队列浅析

    背景 因为在工作中经常会用到阻塞队列,有的时候还要根据业务场景获取重写阻塞队列中的方法,所以学习一下阻塞队列的实现原理还是很有必要的.(PS:不深入了解的话,很容易使用出错,造成没有技术深度的样子) ...

  5. Java并发之ThreadPoolExecutor 线程执行服务

    package com.thread.test.thread; import java.util.concurrent.ExecutorService; import java.util.concur ...

  6. java并发之固定对象与实例

    java并发之固定对象与实例 Immutable Objects An object is considered immutable if its state cannot change after ...

  7. (原创)JAVA阻塞队列LinkedBlockingQueue 以及非阻塞队列ConcurrentLinkedQueue 的区别

    阻塞队列:线程安全 按 FIFO(先进先出)排序元素.队列的头部 是在队列中时间最长的元素.队列的尾部 是在队列中时间最短的元素.新元素插入到队列的尾部,并且队列检索操作会获得位于队列头部的元素.链接 ...

  8. Java并发之CyclicBarria的使用(二)

    Java并发之CyclicBarria的使用(二) 一.简介 之前借助于其他大神写过一篇关于CyclicBarria用法的博文,但是内心总是感觉丝丝的愧疚,因为笔者喜欢原创,而不喜欢去转载一些其他的文 ...

  9. Java并发之CyclicBarria的使用

    Java并发之CyclicBarria的使用 一.简介 笔者在写CountDownLatch这个类的时候,看到了博客园上的<浅析Java中CountDownLatch用法>这篇博文,为博主 ...

随机推荐

  1. WCF 404.3 MIME 映射错误

    WCF部署在IIS下,报错如下: HTTP 错误 404.3 - Not Found由于扩展配置问题而无法提供您请求的页面.如果该页面是脚本,请添加处理程序.如果应下载文件,请添加 MIME 映射. ...

  2. 斯坦福《机器学习》Lesson4感想--1、Logistic回归中的牛顿方法

    在上一篇中提到的Logistic回归是利用最大似然概率的思想和梯度上升算法确定θ,从而确定f(θ).本篇将介绍还有一种求解最大似然概率ℓ(θ)的方法,即牛顿迭代法. 在牛顿迭代法中.如果一个函数是,求 ...

  3. mysql kill process解决死锁

    mysql使用myisam的时候锁表比较多,尤其有慢查询的时候,造成死锁.这时需要手动kill掉locked的process.使他释放. (以前我都是重起服务)..惭愧啊.. 演示:(id 7是我用p ...

  4. MySql 删除相同前缀的表名

    SELECT CONCAT('drop table ', table_name, ';') FROM information_schema.tables WHERE table_name LIKE ' ...

  5. mysql 杀掉(kill) lock进程脚本

    杀掉lock进程最快的方法是重启mysql,像你这种情况,1000多sql锁住了,最好是重启如果不允许重启,我提供一个shell脚本,生成 kill id命令杀掉lock线程,如下:--------- ...

  6. 测试用例使用传统excel还是思维导图(Xmind、MindManager等)?

    一.使用感言 实习时随便使用了word文档编写测试用例,也没有人带.后来第一份正式测试工作,也没有人带测试,那时跟着大众学用思维导图写测试用例,发现思维导图非常灵活.目前使用xmind. 使用思维导图 ...

  7. SQL的四种连接

    SQL的四种连接-内连接.左外连接.右外连接.全连接   今天在看一个遗留系统的数据表的时候发现平时查找的视图是FULL OUT JOIN的,导致平时的数据记录要进行一些限制性处理,其实也可以设置视图 ...

  8. UVa 437 The Tower of Babylon(DP 最长条件子序列)

     题意  给你n种长方体  每种都有无穷个  当一个长方体的长和宽都小于还有一个时  这个长方体能够放在还有一个上面  要求输出这样累积起来的最大高度 由于每一个长方体都有3种放法  比較不好控制 ...

  9. LVM卷组命令

    一般维护命令  #vgscan //检測系统中全部磁盘  #vgck [卷组名] //用于检查卷组中卷组描写叙述区域信息的一致性.  #vgdisplay [卷组名] //显示卷组的属性信息  #vg ...

  10. freemarker 展示数据列表并传值给后台

    select id="initiatorId" name="initiatorId">                  <#if initiato ...