上一篇博客,我们介绍了ArrayBlockQueue,知道了它是基于数组实现的有界阻塞队列,既然有基于数组实现的,那么一定有基于链表实现的队列了,没错,当然有,这就是我们今天的主角:LinkedBlockingQueue。ArrayBlockQueue是有界的,那么LinkedBlockingQueue是有界还是无界的呢?我觉得可以说是有界的,也可以说是无界的,为什么这么说呢?看下去你就知道了。

和上篇博客一样,我们还是先看下LinkedBlockingQueue的基本应用,然后解析LinkedBlockingQueue的核心代码。

LinkedBlockingQueue基本应用

  1. public static void main(String[] args) throws InterruptedException {
  2. LinkedBlockingQueue<Integer> linkedBlockingQueue = new LinkedBlockingQueue();
  3. linkedBlockingQueue.add(15);
  4. linkedBlockingQueue.add(60);
  5. linkedBlockingQueue.offer(50);
  6. linkedBlockingQueue.put(100);
  7. System.out.println(linkedBlockingQueue);
  8. System.out.println(linkedBlockingQueue.size());
  9. System.out.println(linkedBlockingQueue.take());
  10. System.out.println(linkedBlockingQueue);
  11. System.out.println(linkedBlockingQueue.poll());
  12. System.out.println(linkedBlockingQueue);
  13. System.out.println(linkedBlockingQueue.peek());
  14. System.out.println(linkedBlockingQueue);
  15. System.out.println(linkedBlockingQueue.remove(50));
  16. System.out.println(linkedBlockingQueue);
  17. }

运行结果:

  1. [15, 60, 50, 100]
  2. 4
  3. 15
  4. [60, 50, 100]
  5. 60
  6. [50, 100]
  7. 50
  8. [50, 100]
  9. true
  10. [100]

代码比较简单,先试着分析下:

  1. 创建了一个LinkedBlockingQueue 。
  2. 分别使用add/offer/put方法向LinkedBlockingQueue中添加元素,其中add方法执行了两次。
  3. 打印出LinkedBlockingQueue:[15, 60, 50, 100]。
  4. 打印出LinkedBlockingQueue的size:4。
  5. 使用take方法弹出第一个元素,并打印出来:15。
  6. 打印出LinkedBlockingQueue:[60, 50, 100]。
  7. 使用poll方法弹出第一个元素,并打印出来:60。
  8. 打印出LinkedBlockingQueue:[50, 100]。
  9. 使用peek方法弹出第一个元素,并打印出来:50。
  10. 打印出LinkedBlockingQueue:[50, 100]。
  11. 使用remove方法,移除值为50的元素,返回true。
  12. 打印出LinkedBlockingQueue:100。

代码比较简单,但是还是有些细节不明白:

  • 底层是如何保证线程安全性的?
  • 数据保存在哪里,以什么形式保存的?
  • offer/add/put都是往队列里面添加元素,区别是什么?
  • poll/take/peek都是弹出队列的元素,区别是什么?

要解决上面的疑问,最好的途径还是看源码,下面我们就来看看LinkedBlockingQueue的核心源码。

LinkedBlockingQueue源码解析

构造方法

LinkedBlockingQueue提供了三个构造方法,如下图所示:



我们一个一个来分析。

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

无参的构造方法竟然直接把“锅”甩出去了,甩给了另外一个构造方法,但是我们要注意传的参数:Integer.MAX_VALUE。

LinkedBlockingQueue(int capacity)
  1. public LinkedBlockingQueue(int capacity) {
  2. if (capacity <= 0) throw new IllegalArgumentException();
  3. this.capacity = capacity;
  4. last = head = new Node<E>(null);
  5. }
  1. 判断传入的capacity是否合法,如果不大于0,直接抛出异常。
  2. 把传入的capacity赋值给capacity。
  3. 新建一个Node节点,并且把此节点赋值给head和last字段。

这个capacity是什么呢?如果大家对代码有一定的感觉的话,应该很容易猜到这是LinkedBlockingQueue的最大容量。如果我们调用无参的构造方法来创建LinkedBlockingQueue的话,那么它的最大容量就是Integer.MAX_VALUE,我们把它称为“无界”,但是我们也可以指定最大容量,那么此队列又是一个“有界”队列了,所以有些博客很草率的说LinkedBlockingQueue是有界队列,或者是无界队列,个人认为这是不严谨的。

我们再来看看这个Node是个什么鬼:

  1. static class Node<E> {
  2. E item;
  3. Node<E> next;
  4. Node(E x) { item = x; }
  5. }

是不是有一种莫名的亲切感,很明显,这是单向链表的实现呀,next指向的就是下一个Node。

LinkedBlockingQueue(Collection<? extends E> c)
  1. public LinkedBlockingQueue(Collection<? extends E> c) {
  2. this(Integer.MAX_VALUE);//调用第二个构造方法,传入的capacity是Int的最大值,可以说 是一个无界队列。
  3. final ReentrantLock putLock = this.putLock;
  4. putLock.lock(); //开启排他锁
  5. try {
  6. int n = 0;//用于记录LinkedBlockingQueue的size
  7. //循环传入的c集合
  8. for (E e : c) {
  9. if (e == null)//如果e==null,则抛出空指针异常
  10. throw new NullPointerException();
  11. if (n == capacity)//如果n==capacity,说明到了最大的容量,则抛出“Queue full”异常
  12. throw new IllegalStateException("Queue full");
  13. enqueue(new Node<E>(e));//入队操作
  14. ++n;//n自增
  15. }
  16. count.set(n);//设置count
  17. } finally {
  18. putLock.unlock();//释放排他锁
  19. }
  20. }
  1. 调用第二个构造方法,传入了int的最大值,所以可以说此时LinkedBlockingQueue是无界队列。
  2. 开启排他锁putLock 。
  3. 定义了一个变量n,用来记录当前LinkedBlockingQueue的size。
  4. 循环传入的集合,如果其中的元素为null,则抛出空指针异常,如果n==capacity,说明到了最大的容量,则抛出“Queue full”异常,否则执行enqueue操作来进行入队,然后n进行自增。
  5. 设置count为n,由此可知,count就是LinkedBlockingQueue的size了。
  6. 在finally中释放排他锁putLock 。

offer

  1. public boolean offer(E e) {
  2. if (e == null) throw new NullPointerException();//如果传入的元素为NULL,抛出异常
  3. final AtomicInteger count = this.count;//取出count
  4. if (count.get() == capacity)//如果count==capacity,说明到了最大容量,直接返回false
  5. return false;
  6. int c = -1;//表示size
  7. Node<E> node = new Node<E>(e);//新建Node节点
  8. final ReentrantLock putLock = this.putLock;
  9. putLock.lock();//开启排他锁
  10. try {
  11. if (count.get() < capacity) {//如果count<capacity,说明还没有达到最大容量
  12. enqueue(node);//入队操作
  13. c = count.getAndIncrement();//获得count,赋值给c后完成自增操作
  14. if (c + 1 < capacity)//如果c+1 <capacity,说明还有剩余的空间,唤醒因为调用notFull的await方法而被阻塞的线程
  15. notFull.signal();
  16. }
  17. } finally {
  18. putLock.unlock();//在finally中释放排他锁
  19. }
  20. if (c == 0)//如果c==0,说明释放putLock的时候,队列中有一个元素,则调用signalNotEmpty
  21. signalNotEmpty();
  22. return c >= 0;
  23. }
  1. 如果传进来的元素为null,则抛出异常。
  2. 把本类实例的count赋值给局部变量count。
  3. 如果count==capacity,说明到了最大的容量,直接返回false。
  4. 定义局部变量c,用来表示size,初始值是-1。
  5. 新建Node节点。
  6. 开启排他锁putLock。
  7. 如果count>=capacity,说明到了最大的容量,释放排他锁后,返回false,因为此时c=-1,c>=0为false;如果count<capacity,说明还有剩余空间,继续往下执行。这里需要思考一个问题,为什么第三步已经判断过了是否还有剩余空间,这里还要再判断一次呢?因为可能有多个线程都在执行add/offer/put方法,当队列没有满的时候,多个线程同时执行到第三步(第三步的时候还没有开启排他锁),然后同时往下走,所以开启排他锁后,还需要重新判断下。
  8. 执行入队操作。
  9. 获得count,并且赋值给c后,完成自增的操作。注意,是先赋值后自增,赋值和自增的先后顺序会直接影响到后面的判断逻辑。
  10. 如果c+1<capacity,说明还有剩余的空间,唤醒因为调用notFull的await方法而被阻塞的线程。这里为什么要+1再进行判断?因为在第9步中,是先赋值后自增,也就是说局部变量c保存的还是入队之前LinkedBlockingQueue的size,所以要先进行+1操作,得到的才是当前LinkedBlockingQueue的size。
  11. 在finally中,释放排他锁putLock。
  12. 如果c==0,说明在释放putLock排他锁的时候,队列中有且只有一个元素,则调用signalNotEmpty方法。让我们来看看signalNotEmpty方法:
  1. private void signalNotEmpty() {
  2. final ReentrantLock takeLock = this.takeLock;
  3. takeLock.lock();
  4. try {
  5. notEmpty.signal();
  6. } finally {
  7. takeLock.unlock();
  8. }
  9. }

代码比较简单,就是开启排他锁,唤醒因为调用notEmpty的await方法而被阻塞的线程,但是这里需要注意,这里获得的排他锁已经不再是putLock,而是takeLock。

add

  1. public boolean add(E e) {
  2. if (offer(e))
  3. return true;
  4. else
  5. throw new IllegalStateException("Queue full");
  6. }

add方法直接调用了offer方法,但是add和offer还不完全一样,当队列满了,如果调用offer方法,会直接返回false,但是调用add方法,会抛出"Queue full"的异常。

put

  1. public void put(E e) throws InterruptedException {
  2. if (e == null) throw new NullPointerException();//如果传入的元素为NULL,抛出异常
  3. int c = -1;//表示size
  4. Node<E> node = new Node<E>(e);//新建Node节点
  5. final ReentrantLock putLock = this.putLock;
  6. final AtomicInteger count = this.count;//获得count
  7. putLock.lockInterruptibly();//开启排他锁
  8. try {
  9. //如果到了最大容量,调用notFull的await方法,等待唤醒,用while循环
  10. while (count.get() == capacity) {
  11. notFull.await();
  12. }
  13. enqueue(node);//入队
  14. c = count.getAndIncrement();//count先赋值给c后,再进行自增操作
  15. if (c + 1 < capacity)//如果c+1<capacity,调用notFull的signal方法,唤醒因为调用notFull的await方法而被阻塞的线程
  16. notFull.signal();
  17. } finally {
  18. putLock.unlock();//释放排他锁
  19. }
  20. if (c == 0)//如果队列中有一个元素,唤醒因为调用notEmpty的await方法而被阻塞的线程
  21. signalNotEmpty();
  22. }
  1. 如果传入的元素为NULL,则抛出异常。
  2. 定义一个局部变量c,来表示size,初始值是-1。
  3. 新建Node节点。
  4. 把本类实例中的count赋值给局部变量count。
  5. 开启排他锁putLock。
  6. 如果到了最大容量,则调用notFull的await方法,阻塞当前线程,等待其他线程调用notFull的signal方法来唤醒自己。
  7. 执行入队操作。
  8. count先赋值给c后,再进行自增操作。
  9. 如果c+1<capacity,说明还有剩余的空间,则调用notFull的signal方法,唤醒因为调用notFull的await方法而被阻塞的线程。
  10. 释放排他锁putLock。
  11. 如果队列中有且只有一个元素,唤醒因为调用notEmpty的await方法而被阻塞的线程。

enqueue

  1. private void enqueue(Node<E> node) {
  2. last = last.next = node;
  3. }

入队操作是不是特别简单,就是把传入的Node节点,赋值给last节点的next字段,再赋值给last字段,从而形成一个单向链表。

小总结

至此offer/add/put的核心源码已经分析完毕,我们来做一个小总结,offer/add/put都是添加元素的方法,不过他们之间还是有所区别的,当队列满了,调用以上三个方法会出现不同的情况:

  • offer:直接返回false。
  • add:虽然内部也调用了offer方法,但是队列满了,会抛出异常。
  • put:线程会阻塞住,等待唤醒。

size

  1. public int size() {
  2. return count.get();
  3. }

没什么好说的,count记录着LinkedBlockingQueue的size,获得后返回就是了。

take

  1. public E take() throws InterruptedException {
  2. E x;
  3. int c = -1;//size
  4. final AtomicInteger count = this.count;//获得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)//如果size>1,说明在出队之前,队列中有至少两个元素
  14. notEmpty.signal();//唤醒因为调用notEmpty的await方法而被阻塞的线程
  15. } finally {
  16. takeLock.unlock();//释放排他锁
  17. }
  18. if (c == capacity)//如果队列中还有一个剩余空间
  19. signalNotFull();
  20. return x;
  21. }
  1. 定义局部变量c,用来表示size,初始值是-1。
  2. 把本类实例的count字段赋值给临时变量count。
  3. 开启响应中断的排他锁takeLock 。
  4. 如果count==0,说明目前队列中没有数据,就阻塞当前线程,等待唤醒,直到其他线程调用了notEmpty的signal方法唤醒了当前线程。
  5. 进行出队操作。
  6. count先赋值给c后,在进行自减操作,这里需要注意是先赋值,后自减。
  7. 如果c>1,也就是size>1,结合上面的先赋值,后自减,可知如果满足条件,说明在出队之前,队列中至少有两个元素,则调用notEmpty的signal方法,唤醒因为调用notEmpty的await方法而被阻塞的线程。
  8. 释放排他锁takeLock 。
  9. 如果执行出队后,队列中有且只有一个剩余空间,换个说法,就是执行出队操作前,队列是满的,则调用signalNotFull方法。

我们再来看下signalNotFull方法:

  1. private void signalNotFull() {
  2. final ReentrantLock putLock = this.putLock;
  3. putLock.lock();
  4. try {
  5. notFull.signal();
  6. } finally {
  7. putLock.unlock();
  8. }
  9. }
  1. 开启排他锁,注意这里的排他锁是putLock 。
  2. 调用notFull的signal方法,唤醒因为调用notFull的await方法而被阻塞的线程。
  3. 释放排他锁putLock 。

poll

  1. public E poll() {
  2. final AtomicInteger count = this.count;
  3. if (count.get() == 0)
  4. return null;
  5. E x = null;
  6. int c = -1;
  7. final ReentrantLock takeLock = this.takeLock;
  8. takeLock.lock();
  9. try {
  10. if (count.get() > 0) {
  11. x = dequeue();
  12. c = count.getAndDecrement();
  13. if (c > 1)
  14. notEmpty.signal();
  15. }
  16. } finally {
  17. takeLock.unlock();
  18. }
  19. if (c == capacity)
  20. signalNotFull();
  21. return x;
  22. }

相比take方法,最大的区别就如果队列为空,执行take方法会阻塞当前线程,直到被唤醒,而poll方法,直接返回null。

peek

  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<E> first = head.next;
  8. if (first == null)
  9. return null;
  10. else
  11. return first.item;
  12. } finally {
  13. takeLock.unlock();
  14. }
  15. }

peek方法,只是拿到头节点的值,但是不会移除该节点。

dequeue

  1. private E dequeue() {
  2. Node<E> h = head;
  3. Node<E> first = h.next;
  4. h.next = h; // help GC
  5. head = first;
  6. E x = first.item;
  7. first.item = null;
  8. return x;
  9. }

没什么好说的,就是弹出元素,并且移除弹出的元素。

小总结

至此take/poll/peek的核心源码已经分析完毕,我们来做一个小总结,take/poll/peek都是获得头节点值的方法,不过他们之间还是有所区别的:

  • take:当队列为空,会阻塞当前线程,直到被唤醒。会进行出队操作,移除获得的节点。
  • poll:当队列为空,直接返回null。会进行出队操作,移除获得的节点。
  • peek:当队列为空,直接返回null。不会移除节点。

LinkedBlockingQueue的核心源码分析到这里完毕了,谢谢大家。

LinkedBlockingQueue源码解析的更多相关文章

  1. LinkedBlockingQueue源码解析(3)

    此文已由作者赵计刚授权网易云社区发布. 欢迎访问网易云社区,了解更多网易技术产品运营经验. 4.3.public E take() throws InterruptedException 原理: 将队 ...

  2. 第九章 LinkedBlockingQueue源码解析

    1.对于LinkedBlockingQueue需要掌握以下几点 创建 入队(添加元素) 出队(删除元素) 2.创建 Node节点内部类与LinkedBlockingQueue的一些属性 static ...

  3. Java并发包源码学习系列:阻塞队列实现之LinkedBlockingQueue源码解析

    目录 LinkedBlockingQueue概述 类图结构及重要字段 构造器 出队和入队操作 入队enqueue 出队dequeue 阻塞式操作 E take() 阻塞式获取 void put(E e ...

  4. LinkedBlockingQueue源码解析(1)

    此文已由作者赵计刚授权网易云社区发布. 欢迎访问网易云社区,了解更多网易技术产品运营经验. 1.对于LinkedBlockingQueue需要掌握以下几点 创建 入队(添加元素) 出队(删除元素) 2 ...

  5. LinkedBlockingQueue源码解析(2)

    此文已由作者赵计刚授权网易云社区发布. 欢迎访问网易云社区,了解更多网易技术产品运营经验. 3.3.public void put(E e) throws InterruptedException 原 ...

  6. Java并发包源码学习系列:阻塞队列实现之PriorityBlockingQueue源码解析

    目录 PriorityBlockingQueue概述 类图结构及重要字段 什么是二叉堆 堆的基本操作 向上调整void up(int u) 向下调整void down(int u) 构造器 扩容方法t ...

  7. Java并发包源码学习系列:阻塞队列实现之DelayQueue源码解析

    目录 DelayQueue概述 类图及重要字段 Delayed接口 Delayed元素案例 构造器 put take first = null 有什么用 总结 参考阅读 系列传送门: Java并发包源 ...

  8. Java并发包源码学习系列:阻塞队列实现之SynchronousQueue源码解析

    目录 SynchronousQueue概述 使用案例 类图结构 put与take方法 void put(E e) E take() Transfer 公平模式TransferQueue QNode t ...

  9. Java并发包源码学习系列:阻塞队列实现之LinkedTransferQueue源码解析

    目录 LinkedTransferQueue概述 TransferQueue 类图结构及重要字段 Node节点 前置:xfer方法的定义 队列操作三大类 插入元素put.add.offer 获取元素t ...

随机推荐

  1. Extjs 上传文件 IE不兼容的问题[提示下载保存]

    我最不喜欢的浏览器的是IE,但无奈很多项目的客户使用的是IE. 在使用Extjs做文件上传时,其他浏览器没有问题,但IE却一个劲提示保存文件,看服务端运行,它其实是运行成功了已经,但客户端的进度条却一 ...

  2. springMVC框架在js中使用window.location.href请求url时IE不兼容问题解决

    是使用springMVC框架时,有时候需要在js中使用window.location.href来请求url,比如下面的路径: window.location.href = 'forecast/down ...

  3. VM虚拟机安装centos详细图文教程

    本教程贴,采用VM虚拟机进行安装, Ps:不懂VM使用的,可以百度一下 第一步,启动虚拟机,并进行新建---虚拟机·· 选择 从镜像安装,吧里有6.3镜像下载的链接的 然后, 下一步 . 选择客户机版 ...

  4. Java 读书笔记 (十五) Java 异常处理

    捕获异常 使用try 和catch关键字可以捕获异常.try/catch 代码块放在异常可能发生的地方. try/catch 代码块中的代码称为保护代码 ,使用try/catch的语法如下: try ...

  5. segment.go

    package sego // 文本中的一个分词 type Segment struct {     // 分词在文本中的起始字节位置     start int     // 分词在文本中的结束字节 ...

  6. error.go源码笔记

    ] {     case errorCodeConnFailed:         return ErrConnectionFailed(err)     case errorCodeHttpServ ...

  7. golang接口三个特性

    类型和接口 因为映射建设在类型的基础之上,首先我们对类型进行全新的介绍.go是一个静态性语言,每个变量都有静态的类型,因此每个变量在编译阶段中有明确的变量类型,比如像:int.float32.MyTy ...

  8. 在C++中怎么输入反斜杠“ \ ”

    在C++编程中有时就会遇到有些符号不能直接输入,像反斜杠“ \ ",如果直接输入会出现:错误的终止了宏调用的错误. 这时,我们就需要把这些符号转义一下, 例如: CString str = ...

  9. 【爆料】-《维多利亚大学毕业证书》Victoria一模一样原件

    ☞维多利亚大学毕业证书[微/Q:865121257◆WeChat:CC6669834]UC毕业证书/联系人Alice[查看点击百度快照查看][留信网学历认证&博士&硕士&海归& ...

  10. MIP 脚本域名地址变更公告

    尊敬的 MIP 开发者: MIP 团队为了解决 MIP-Cache 页面下 cookie 相互覆盖问题,增强站点品牌露出,在 2017 年 8 月将 MIP 的脚本域名和 MIP-Cache 页面域名 ...