一、前言

  分析完了ArrayBlockingQueue后,接着分析LinkedBlockingQueue,与ArrayBlockingQueue不相同,LinkedBlockingQueue底层采用的是链表结构,其源码也相对比较简单,下面进行正式的分析。

二、LinkedBlockingQueue数据结构

  从LinkedBlockingQueue的命名就大致知道其数据结构采用的是链表结构,通过源码也可以验证我们的猜测,其数据结构如下。

  说明:可以看到LinkedBlockingQueue采用的是单链表结构,包含了头结点和尾节点。

三、LinkedBlockingQueue源码分析

  3.1 类的继承关系  

  1. public class LinkedBlockingQueue<E> extends AbstractQueue<E>
  2. implements BlockingQueue<E>, java.io.Serializable {}

  说明:LinkedBlockingQueue继承了AbstractQueue抽象类,AbstractQueue定义了对队列的基本操作;同时实现了BlockingQueue接口,BlockingQueue表示阻塞型的队列,其对队列的操作可能会抛出异常;同时也实现了Searializable接口,表示可以被序列化。

  3.2 类的内部类

  LinkedBlockingQueue内部有一个Node类,表示结点,用于存放元素,其源码如下。  

  1. static class Node<E> {
  2. // 元素
  3. E item;
  4. // next域
  5. Node<E> next;
  6. // 构造函数
  7. Node(E x) { item = x; }
  8. }

  说明:Node类非常简单,包含了两个域,分别用于存放元素和指示下一个结点。

  3.3 类的属性  

  1. public class LinkedBlockingQueue<E> extends AbstractQueue<E>
  2. implements BlockingQueue<E>, java.io.Serializable {
  3. // 版本序列号
  4. private static final long serialVersionUID = -6903933977591709194L;
  5. // 容量
  6. private final int capacity;
  7. // 元素的个数
  8. private final AtomicInteger count = new AtomicInteger();
  9. // 头结点
  10. transient Node<E> head;
  11. // 尾结点
  12. private transient Node<E> last;
  13. // 取元素锁
  14. private final ReentrantLock takeLock = new ReentrantLock();
  15. // 非空条件
  16. private final Condition notEmpty = takeLock.newCondition();
  17. // 存元素锁
  18. private final ReentrantLock putLock = new ReentrantLock();
  19. // 非满条件
  20. private final Condition notFull = putLock.newCondition();
  21. }

  说明:可以看到LinkedBlockingQueue包含了读、写重入锁(与ArrayBlockingQueue不同,ArrayBlockingQueue只包含了一把重入锁),读写操作进行了分离,并且不同的锁有不同的Condition条件(与ArrayBlockingQueue不同,ArrayBlockingQueue是一把重入锁的两个条件)。

  3.4 类的构造函数

  1. LinkedBlockingQueue()型构造函数  

  1. public LinkedBlockingQueue() {
  2. this(Integer.MAX_VALUE);
  3. }

  说明:该构造函数用于创建一个容量为 Integer.MAX_VALUE 的 LinkedBlockingQueue。

  2. LinkedBlockingQueue(int)型构造函数  

  1. public LinkedBlockingQueue(int capacity) {
  2. // 初始化容量必须大于0
  3. if (capacity <= 0) throw new IllegalArgumentException();
  4. // 初始化容量
  5. this.capacity = capacity;
  6. // 初始化头结点和尾结点
  7. last = head = new Node<E>(null);
  8. }

  说明:该构造函数用于创建一个具有给定(固定)容量的 LinkedBlockingQueue。

  3. LinkedBlockingQueue(Collection<? extends E>)型构造函数 

  1. public LinkedBlockingQueue(Collection<? extends E> c) {
  2. // 调用重载构造函数
  3. this(Integer.MAX_VALUE);
  4. // 存锁
  5. final ReentrantLock putLock = this.putLock;
  6. // 获取锁
  7. putLock.lock(); // Never contended, but necessary for visibility
  8. try {
  9. int n = 0;
  10. for (E e : c) { // 遍历c集合
  11. if (e == null) // 元素为null,抛出异常
  12. throw new NullPointerException();
  13. if (n == capacity) //
  14. throw new IllegalStateException("Queue full");
  15. enqueue(new Node<E>(e));
  16. ++n;
  17. }
  18. count.set(n);
  19. } finally {
  20. putLock.unlock();
  21. }
  22. }

  说明:该构造函数用于创建一个容量是 Integer.MAX_VALUE 的 LinkedBlockingQueue,最初包含给定 collection 的元素,元素按该 collection 迭代器的遍历顺序添加。

  3.5 核心函数分析

  1. put函数  

  1. public void put(E e) throws InterruptedException {
  2. // 值不为空
  3. if (e == null) throw new NullPointerException();
  4. // Note: convention in all put/take/etc is to preset local var
  5. // holding count negative to indicate failure unless set.
  6. //
  7. int c = -1;
  8. // 新生结点
  9. Node<E> node = new Node<E>(e);
  10. // 存元素锁
  11. final ReentrantLock putLock = this.putLock;
  12. // 元素个数
  13. final AtomicInteger count = this.count;
  14. // 如果当前线程未被中断,则获取锁
  15. putLock.lockInterruptibly();
  16. try {
  17. /*
  18. * Note that count is used in wait guard even though it is
  19. * not protected by lock. This works because count can
  20. * only decrease at this point (all other puts are shut
  21. * out by lock), and we (or some other waiting put) are
  22. * signalled if it ever changes from capacity. Similarly
  23. * for all other uses of count in other wait guards.
  24. */
  25. while (count.get() == capacity) { // 元素个数到达指定容量
  26. // 在notFull条件上进行等待
  27. notFull.await();
  28. }
  29. // 入队列
  30. enqueue(node);
  31. // 更新元素个数,返回的是以前的元素个数
  32. c = count.getAndIncrement();
  33. if (c + 1 < capacity) // 元素个数是否小于容量
  34. // 唤醒在notFull条件上等待的某个线程
  35. notFull.signal();
  36. } finally {
  37. // 释放锁
  38. putLock.unlock();
  39. }
  40. if (c == 0) // 元素个数为0,表示已有take线程在notEmpty条件上进入了等待,则需要唤醒在notEmpty条件上等待的线程
  41. signalNotEmpty();
  42. }

  说明:put函数用于存放元素,其流程如下。

  ① 判断元素是否为null,若是,则抛出异常,否则,进入步骤②

  ② 获取存元素锁,并上锁,如果当前线程被中断,则抛出异常,否则,进入步骤③

  ③ 判断当前队列中的元素个数是否已经达到指定容量,若是,则在notFull条件上进行等待,否则,进入步骤④

  ④ 将新生结点入队列,更新队列元素个数,若元素个数小于指定容量,则唤醒在notFull条件上等待的线程,表示可以继续存放元素。进入步骤⑤

  ⑤ 释放锁,判断结点入队列之前的元素个数是否为0,若是,则唤醒在notEmpty条件上等待的线程(表示队列中没有元素,取元素线程被阻塞了)。

  put函数中会调用到enqueue函数和signalNotEmpty函数,enqueue函数源码如下  

  1. private void enqueue(Node<E> node) {
  2. // assert putLock.isHeldByCurrentThread();
  3. // assert last.next == null;
  4. // 更新尾结点域
  5. last = last.next = node;
  6. }

  说明:可以看到,enqueue函数只是更新了尾节点。signalNotEmpty函数源码如下 

  1. private void signalNotEmpty() {
  2. // 取元素锁
  3. final ReentrantLock takeLock = this.takeLock;
  4. // 获取锁
  5. takeLock.lock();
  6. try {
  7. // 唤醒在notEmpty条件上等待的某个线程
  8. notEmpty.signal();
  9. } finally {
  10. // 释放锁
  11. takeLock.unlock();
  12. }
  13. }

  说明:signalNotEmpty函数用于唤醒在notEmpty条件上等待的线程,其首先获取取元素锁,然后上锁,然后唤醒在notEmpty条件上等待的线程,最后释放取元素锁。

  2. offer函数 

  1. public boolean offer(E e) {
  2. // 确保元素不为null
  3. if (e == null) throw new NullPointerException();
  4. // 获取计数器
  5. final AtomicInteger count = this.count;
  6. if (count.get() == capacity) // 元素个数到达指定容量
  7. // 返回
  8. return false;
  9. //
  10. int c = -1;
  11. // 新生结点
  12. Node<E> node = new Node<E>(e);
  13. // 存元素锁
  14. final ReentrantLock putLock = this.putLock;
  15. // 获取锁
  16. putLock.lock();
  17. try {
  18. if (count.get() < capacity) { // 元素个数小于指定容量
  19. // 入队列
  20. enqueue(node);
  21. // 更新元素个数,返回的是以前的元素个数
  22. c = count.getAndIncrement();
  23. if (c + 1 < capacity) // 元素个数是否小于容量
  24. // 唤醒在notFull条件上等待的某个线程
  25. notFull.signal();
  26. }
  27. } finally {
  28. // 释放锁
  29. putLock.unlock();
  30. }
  31. if (c == 0) // 元素个数为0,则唤醒在notEmpty条件上等待的某个线程
  32. signalNotEmpty();
  33. return c >= 0;
  34. }

  说明:offer函数也用于存放元素,offer函数添加元素不会抛出异常(其他的域put函数类似)。

  3. take函数

  1. public E take() throws InterruptedException {
  2. E x;
  3. int c = -1;
  4. // 获取计数器
  5. final AtomicInteger count = this.count;
  6. // 获取取元素锁
  7. final ReentrantLock takeLock = this.takeLock;
  8. // 如果当前线程未被中断,则获取锁
  9. takeLock.lockInterruptibly();
  10. try {
  11. while (count.get() == 0) { // 元素个数为0
  12. // 在notEmpty条件上等待
  13. notEmpty.await();
  14. }
  15. // 出队列
  16. x = dequeue();
  17. // 更新元素个数,返回的是以前的元素个数
  18. c = count.getAndDecrement();
  19. if (c > 1) // 元素个数大于1,则唤醒在notEmpty上等待的某个线程
  20. notEmpty.signal();
  21. } finally {
  22. // 释放锁
  23. takeLock.unlock();
  24. }
  25. if (c == capacity) // 元素个数到达指定容量
  26. // 唤醒在notFull条件上等待的某个线程
  27. signalNotFull();
  28. // 返回
  29. return x;
  30. }

  说明:take函数用于获取一个元素,其与put函数相对应,其流程如下。

  ① 获取取元素锁,并上锁,如果当前线程被中断,则抛出异常,否则,进入步骤②

  ② 判断当前队列中的元素个数是否为0,若是,则在notEmpty条件上进行等待,否则,进入步骤③

  ③ 出队列,更新队列元素个数,若元素个数大于1,则唤醒在notEmpty条件上等待的线程,表示可以继续取元素。进入步骤④

  ④ 释放锁,判断结点出队列之前的元素个数是否为指定容量,若是,则唤醒在notFull条件上等待的线程(表示队列已满,存元素线程被阻塞了)。

  take函数调用到了dequeue函数和signalNotFull函数,dequeue函数源码如下  

  1. private E dequeue() {
  2. // assert takeLock.isHeldByCurrentThread();
  3. // assert head.item == null;
  4. // 头结点
  5. Node<E> h = head;
  6. // 第一个结点
  7. Node<E> first = h.next;
  8. // 头结点的next域为自身
  9. h.next = h; // help GC
  10. // 更新头结点
  11. head = first;
  12. // 返回头结点的元素
  13. E x = first.item;
  14. // 头结点的item域赋值为null
  15. first.item = null;
  16. // 返回结点元素
  17. return x;
  18. }

  说明:dequeue函数的作用是将头结点更新为之前头结点的下一个结点,并且将更新后的头结点的item域设置为null。signalNotFull函数的源码如下

  1. private void signalNotFull() {
  2. // 存元素锁
  3. final ReentrantLock putLock = this.putLock;
  4. // 获取锁
  5. putLock.lock();
  6. try {
  7. // 唤醒在notFull条件上等待的某个线程
  8. notFull.signal();
  9. } finally {
  10. // 释放锁
  11. putLock.unlock();
  12. }
  13. }

  说明:signalNotFull函数用于唤醒在notFull条件上等待的某个线程,其首先获取存元素锁,然后上锁,然后唤醒在notFull条件上等待的线程,最后释放存元素锁。

  4. poll函数  

  1. public E poll() {
  2. // 获取计数器
  3. final AtomicInteger count = this.count;
  4. if (count.get() == 0) // 元素个数为0
  5. return null;
  6. //
  7. E x = null;
  8. int c = -1;
  9. // 取元素锁
  10. final ReentrantLock takeLock = this.takeLock;
  11. // 获取锁
  12. takeLock.lock();
  13. try {
  14. if (count.get() > 0) { // 元素个数大于0
  15. // 出队列
  16. x = dequeue();
  17. // 更新元素个数,返回的是以前的元素个数
  18. c = count.getAndDecrement();
  19. if (c > 1) // 元素个数大于1
  20. // 唤醒在notEmpty条件上等待的某个线程
  21. notEmpty.signal();
  22. }
  23. } finally {
  24. // 释放锁
  25. takeLock.unlock();
  26. }
  27. if (c == capacity) // 元素大小达到指定容量
  28. // 唤醒在notFull条件上等待的某个线程
  29. signalNotFull();
  30. // 返回元素
  31. return x;
  32. }

  说明:poll函数也用于存放元素,poll函数添加元素不会抛出异常(其他的与take函数类似)。

  5. remove函数  

  1. public boolean remove(Object o) {
  2. // 元素为null,返回false
  3. if (o == null) return false;
  4. // 获取存元素锁和取元素锁(不允许存或取元素)
  5. fullyLock();
  6. try {
  7. for (Node<E> trail = head, p = trail.next;
  8. p != null;
  9. trail = p, p = p.next) { // 遍历整个链表
  10. if (o.equals(p.item)) { // 结点的值与指定值相等
  11. // 断开结点
  12. unlink(p, trail);
  13. return true;
  14. }
  15. }
  16. return false;
  17. } finally {
  18. fullyUnlock();
  19. }
  20. }

  说明:remove函数的流程如下

  ① 获取读、写锁(防止此时继续出、入队列)。进入步骤②

  ② 遍历链表,寻找指定元素,若找到,则将该结点从链表中断开,有利于被GC,进入步骤③

  ③ 释放读、写锁(可以继续出、入队列)。步骤②中找到指定元素则返回true,否则,返回false。

  其中,remove函数会调用unlink函数,其源码如下  

  1. void unlink(Node<E> p, Node<E> trail) {
  2. // assert isFullyLocked();
  3. // p.next is not changed, to allow iterators that are
  4. // traversing p to maintain their weak-consistency guarantee.
  5. // 结点的item域赋值为null
  6. p.item = null;
  7. // 断开p结点
  8. trail.next = p.next;
  9. if (last == p) // 尾节点为p结点
  10. // 重新赋值尾节点
  11. last = trail;
  12. if (count.getAndDecrement() == capacity) // 更新元素个数,返回的是以前的元素个数,若结点个数到达指定容量
  13. // 唤醒在notFull条件上等待的某个线程
  14. notFull.signal();
  15. }

  说明:unlink函数用于将指定结点从链表中断开,并且更新队列元素个数,并且判断若之前队列元素的个数达到了指定容量,则会唤醒在notFull条件上等待的某个线程。

四、示例

  下面通过一个示例来了解LinkedBlockingQueue的使用。  

  1. package com.hust.grid.leesf.collections;
  2.  
  3. import java.util.concurrent.LinkedBlockingQueue;
  4. class PutThread extends Thread {
  5. private LinkedBlockingQueue<Integer> lbq;
  6. public PutThread(LinkedBlockingQueue<Integer> lbq) {
  7. this.lbq = lbq;
  8. }
  9.  
  10. public void run() {
  11. for (int i = 0; i < 10; i++) {
  12. try {
  13. System.out.println("put " + i);
  14. lbq.put(i);
  15. Thread.sleep(100);
  16. } catch (InterruptedException e) {
  17. e.printStackTrace();
  18. }
  19. }
  20. }
  21. }
  22.  
  23. class GetThread extends Thread {
  24. private LinkedBlockingQueue<Integer> lbq;
  25. public GetThread(LinkedBlockingQueue<Integer> lbq) {
  26. this.lbq = lbq;
  27. }
  28.  
  29. public void run() {
  30. for (int i = 0; i < 10; i++) {
  31. try {
  32. System.out.println("take " + lbq.take());
  33. Thread.sleep(100);
  34. } catch (InterruptedException e) {
  35. e.printStackTrace();
  36. }
  37. }
  38. }
  39. }
  40. public class LinkedBlockingQueueDemo {
  41. public static void main(String[] args) {
  42. LinkedBlockingQueue<Integer> lbq = new LinkedBlockingQueue<Integer>();
  43.  
  44. PutThread p1 = new PutThread(lbq);
  45. GetThread g1 = new GetThread(lbq);
  46.  
  47. p1.start();
  48. g1.start();
  49. }
  50. }

  运行结果:  

  1. put 0
  2. take 0
  3. put 1
  4. take 1
  5. put 2
  6. take 2
  7. put 3
  8. take 3
  9. put 4
  10. take 4
  11. put 5
  12. take 5
  13. put 6
  14. take 6
  15. put 7
  16. take 7
  17. put 8
  18. take 8
  19. put 9
  20. take 9

  说明:示例中使用了两个线程,一个用于存元素,一个用于读元素,存和读各10次,每个线程存一个元素或者读一个元素后都会休眠100ms,可以看到结果是交替打 印,并且首先打印的肯定是put线程语句(因为若取线程先取元素,此时队列并没有元素,其会阻塞,等待存线程存入元素),并且最终程序可以正常结束。

  ① 若修改取元素线程,将存的元素的次数修改为15次(for循环的结束条件改为15即可),运行结果如下:  

  1. put 0
  2. take 0
  3. put 1
  4. take 1
  5. put 2
  6. take 2
  7. put 3
  8. take 3
  9. put 4
  10. take 4
  11. put 5
  12. take 5
  13. put 6
  14. take 6
  15. put 7
  16. take 7
  17. put 8
  18. take 8
  19. put 9
  20. take 9

  说明:运行结果与上面的运行结果相同,但是,此时程序无法正常结束,因为take方法被阻塞了,等待被唤醒。

五、总结

  LinkedBlockingQueue的源码相对比较简单,其也是通过ReentrantLock和Condition条件来保证多线程的正确访问的,并且取元素(出队列)和存元素(入队列)是采用不同的锁,进行了读写分离,有利于提高并发度。LinkedBockingQueue的分析就到这里,欢迎交流,谢谢各位园友的观看~

【JUC】JDK1.8源码分析之LinkedBlockingQueue(四)的更多相关文章

  1. 【1】【JUC】JDK1.8源码分析之ArrayBlockingQueue,LinkedBlockingQueue

    概要: ArrayBlockingQueue的内部是通过一个可重入锁ReentrantLock和两个Condition条件对象来实现阻塞 注意这两个Condition即ReentrantLock的Co ...

  2. 【JUC】JDK1.8源码分析之ArrayBlockingQueue(三)

    一.前言 在完成Map下的并发集合后,现在来分析ArrayBlockingQueue,ArrayBlockingQueue可以用作一个阻塞型队列,支持多任务并发操作,有了之前看源码的积累,再看Arra ...

  3. 【1】【JUC】JDK1.8源码分析之ReentrantLock

    概要: ReentrantLock类内部总共存在Sync.NonfairSync.FairSync三个类,NonfairSync与FairSync类继承自Sync类,Sync类继承自AbstractQ ...

  4. 【集合框架】JDK1.8源码分析HashSet && LinkedHashSet(八)

    一.前言 分析完了List的两个主要类之后,我们来分析Set接口下的类,HashSet和LinkedHashSet,其实,在分析完HashMap与LinkedHashMap之后,再来分析HashSet ...

  5. 【集合框架】JDK1.8源码分析之HashMap(一) 转载

    [集合框架]JDK1.8源码分析之HashMap(一)   一.前言 在分析jdk1.8后的HashMap源码时,发现网上好多分析都是基于之前的jdk,而Java8的HashMap对之前做了较大的优化 ...

  6. 【集合框架】JDK1.8源码分析之ArrayList详解(一)

    [集合框架]JDK1.8源码分析之ArrayList详解(一) 一. 从ArrayList字表面推测 ArrayList类的命名是由Array和List单词组合而成,Array的中文意思是数组,Lis ...

  7. 集合之TreeSet(含JDK1.8源码分析)

    一.前言 前面分析了Set接口下的hashSet和linkedHashSet,下面接着来看treeSet,treeSet的底层实现是基于treeMap的. 四个关注点在treeSet上的答案 二.tr ...

  8. 集合之LinkedHashSet(含JDK1.8源码分析)

    一.前言 上篇已经分析了Set接口下HashSet,我们发现其操作都是基于hashMap的,接下来看LinkedHashSet,其底层实现都是基于linkedHashMap的. 二.linkedHas ...

  9. 集合之HashSet(含JDK1.8源码分析)

    一.前言 我们已经分析了List接口下的ArrayList和LinkedList,以及Map接口下的HashMap.LinkedHashMap.TreeMap,接下来看的是Set接口下HashSet和 ...

随机推荐

  1. Java_equals和“==”的区别

    1. 对于基本数据类型 它们的比较,应该用“==”,比较的是他们的值. 2. 引用数据类型 “==”判断的是对象是否为同一个,也就是它们内存中的存放地址是否一样,一样,则返回true,否则返回fals ...

  2. JDBC增删改查简单测试

    首先编写一个entity以便与数据库表文件相对应 lyTable.java public class LyTable implements java.io.Serializable { private ...

  3. 使用Powershell收集服务器信息

    function Get-OSInfo         ([string]$Server) {           $object = Get-WmiObject  win32_computersys ...

  4. java基本数据类型

    基本数据类型概念 java是一种强类型语言,意味着必须为每一个变量声明一种数据类型. java拥有8中基本数据类型,主要包含如下:4中整形类型(long.int.short.byte)表示整形数值:两 ...

  5. MySQL 处理插入过程中的主键唯一键重复值办法

    200 ? "200px" : this.width)!important;} --> 介绍 本篇文章主要介绍在插入数据到表中遇到键重复避免插入重复值的处理方法,主要涉及到I ...

  6. [OpenGL][SharpGL]用Polygon Offset解决z-fighting和stitching问题

    [OpenGL][SharpGL]用Polygon Offset解决z-fighting和stitching问题 本文参考了(http://www.zeuscmd.com/tutorials/open ...

  7. 2013 duilib入门简明教程 -- 部分bug (11)

     一.WindowImplBase的bug     在第8个教程[2013 duilib入门简明教程 -- 完整的自绘标题栏(8)]中,可以发现窗口最大化之后有两个问题,     1.最大化按钮的样式 ...

  8. python3.5 正则表达式

    我们平时上网的时候,经常需要在一些网站上注册帐号,而注册帐号的时候对帐号名称会有一些要求. 比如: 上面的图片中,输入的邮件地址.密码.手机号 才可以注册成功. 我们需要匹配用户输入的内容,判断用户输 ...

  9. C#设计模式-桥接模式

    这里以电视遥控器的一个例子来引出桥接模式解决的问题,首先,我们每个牌子的电视机都有一个遥控器,此时我们能想到的一个设计是——把遥控器做为一个抽象类,抽象类中提供遥控器的所有实现,其他具体电视品牌的遥控 ...

  10. Juint整合Log4j

    一般Log4j配置在web.xml中,在单元测试时,不需要启动Tomcat,所有Log4j找不到配置文件 在测试类中手动加载 配置文件 PropertyConfigurator.configure(& ...