死磕 java集合之LinkedBlockingQueue源码分析
问题
(1)LinkedBlockingQueue的实现方式?
(2)LinkedBlockingQueue是有界的还是无界的队列?
(3)LinkedBlockingQueue相比ArrayBlockingQueue有什么改进?
简介
LinkedBlockingQueue是java并发包下一个以单链表实现的阻塞队列,它是线程安全的,至于它是不是有界的,请看下面的分析。
源码分析
主要属性
// 容量 private final int capacity; // 元素数量 private final AtomicInteger count = new AtomicInteger(); // 链表头 transient Node<E> head; // 链表尾 private transient Node<E> last; // take锁 private final ReentrantLock takeLock = new ReentrantLock(); // notEmpty条件 // 当队列无元素时,take锁会阻塞在notEmpty条件上,等待其它线程唤醒 private final Condition notEmpty = takeLock.newCondition(); // 放锁 private final ReentrantLock putLock = new ReentrantLock(); // notFull条件 // 当队列满了时,put锁会会阻塞在notFull上,等待其它线程唤醒 private final Condition notFull = putLock.newCondition();
(1)capacity,有容量,可以理解为LinkedBlockingQueue是有界队列
(2)head, last,链表头、链表尾指针
(3)takeLock,notEmpty,take锁及其对应的条件
(4)putLock, notFull,put锁及其对应的条件
(5)入队、出队使用两个不同的锁控制,锁分离,提高效率
内部类
static class Node<E> { E item; Node<E> next; Node(E x) { item = x; } }
典型的单链表结构。
主要构造方法
public LinkedBlockingQueue() { // 如果没传容量,就使用最大int值初始化其容量 this(Integer.MAX_VALUE); } public LinkedBlockingQueue(int capacity) { if (capacity <= 0) throw new IllegalArgumentException(); this.capacity = capacity; // 初始化head和last指针为空值节点 last = head = new Node<E>(null); }
入队
入队同样有四个方法,我们这里只分析最重要的一个,put(E e)方法:
public void put(E e) throws InterruptedException { // 不允许null元素 if (e == null) throw new NullPointerException(); int c = -1; // 新建一个节点 Node<E> node = new Node<E>(e); final ReentrantLock putLock = this.putLock; final AtomicInteger count = this.count; // 使用put锁加锁 putLock.lockInterruptibly(); try { // 如果队列满了,就阻塞在notFull条件上 // 等待被其它线程唤醒 while (count.get() == capacity) { notFull.await(); } // 队列不满了,就入队 enqueue(node); // 队列长度加1 c = count.getAndIncrement(); // 如果现队列长度如果小于容量 // 就再唤醒一个阻塞在notFull条件上的线程 // 这里为啥要唤醒一下呢? // 因为可能有很多线程阻塞在notFull这个条件上的 // 而取元素时只有取之前队列是满的才会唤醒notFull // 为什么队列满的才唤醒notFull呢? // 因为唤醒是需要加putLock的,这是为了减少锁的次数 // 所以,这里索性在放完元素就检测一下,未满就唤醒其它notFull上的线程 // 说白了,这也是锁分离带来的代价 if (c + 1 < capacity) notFull.signal(); } finally { // 释放锁 putLock.unlock(); } // 如果原队列长度为0,现在加了一个元素后立即唤醒notEmpty条件 if (c == 0) signalNotEmpty(); } private void enqueue(Node<E> node) { // 直接加到last后面 last = last.next = node; } private void signalNotEmpty() { final ReentrantLock takeLock = this.takeLock; // 加take锁 takeLock.lock(); try { // 唤醒notEmpty条件 notEmpty.signal(); } finally { // 解锁 takeLock.unlock(); } }
(1)使用putLock加锁;
(2)如果队列满了就阻塞在notFull条件上;
(3)否则就入队;
(4)如果入队后元素数量小于容量,唤醒其它阻塞在notFull条件上的线程;
(5)释放锁;
(6)如果放元素之前队列长度为0,就唤醒notEmpty条件;
出队
出队同样也有四个方法,我们这里只分析最重要的那一个,take()方法:
public E take() throws InterruptedException { E x; int c = -1; final AtomicInteger count = this.count; final ReentrantLock takeLock = this.takeLock; // 使用takeLock加锁 takeLock.lockInterruptibly(); try { // 如果队列无元素,则阻塞在notEmpty条件上 while (count.get() == 0) { notEmpty.await(); } // 否则,出队 x = dequeue(); // 获取出队前队列的长度 c = count.getAndDecrement(); // 如果取之前队列长度大于1,则唤醒notEmpty if (c > 1) notEmpty.signal(); } finally { // 释放锁 takeLock.unlock(); } // 如果取之前队列长度等于容量 // 则唤醒notFull if (c == capacity) signalNotFull(); return x; } private E dequeue() { // head节点本身是不存储任何元素的 // 这里把head删除,并把head下一个节点作为新的值 // 并把其值置空,返回原来的值 Node<E> h = head; Node<E> first = h.next; h.next = h; // help GC head = first; E x = first.item; first.item = null; return x; } private void signalNotFull() { final ReentrantLock putLock = this.putLock; putLock.lock(); try { // 唤醒notFull notFull.signal(); } finally { putLock.unlock(); } }
(1)使用takeLock加锁;
(2)如果队列空了就阻塞在notEmpty条件上;
(3)否则就出队;
(4)如果出队前元素数量大于1,唤醒其它阻塞在notEmpty条件上的线程;
(5)释放锁;
(6)如果取元素之前队列长度等于容量,就唤醒notFull条件;
总结
(1)LinkedBlockingQueue采用单链表的形式实现;
(2)LinkedBlockingQueue采用两把锁的锁分离技术实现入队出队互不阻塞;
(3)LinkedBlockingQueue是有界队列,不传入容量时默认为最大int值;
彩蛋
(1)LinkedBlockingQueue与ArrayBlockingQueue对比?
a)后者入队出队采用一把锁,导致入队出队相互阻塞,效率低下;
b)前才入队出队采用两把锁,入队出队互不干扰,效率较高;
c)二者都是有界队列,如果长度相等且出队速度跟不上入队速度,都会导致大量线程阻塞;
d)前者如果初始化不传入初始容量,则使用最大int值,如果出队速度跟不上入队速度,会导致队列特别长,占用大量内存;
欢迎关注我的公众号“彤哥读源码”,查看更多源码系列文章, 与彤哥一起畅游源码的海洋。
低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
使用lombok编写优雅的Bean对象
使用java编写代码,十之八九都是在写java类,从而构建java对象。lombok之前也说了不少,但使用了这么多年,感觉还是有很多技巧可以使用的。 毫无疑问,使用lombok,编写的java代码很优雅,而使用起来和普通的java编码方式创建的类毫无二致。 不过,这样就满足了吗?实际上lombok很多注解,让这个java类在使用的时候,也可以更优雅。 本文就从ORM实体类、Builder模式工具类、Wither工具类以及Accessors工具类几个层面对比一下。 首先说明,不同的方式本质上没有优劣之分,不过在不同的应用场景就会变得很奇妙了。 ORM实体类 当一个java Bean类作为ORM实体类,或者xml、json的映射类时,需要这个类有这几个特征: 拥有无参构造器 拥有setter方法,用以反序列化; 拥有getter方法,用以序列化。 那么最简单的情况就是: @Data public class UserBean{ private Integer id; private String userName; } 复习一下,Data 注解相当于装配了 @Getter @Setter ...
- 下一篇
【重构】Spring Cloud OAuth 无Token调用源码封装
背景 重构-改善既有代码的设计,重构的目的是是软件更容易被理解和修改。 书接上回Spring Security OAuth 微服务内部Token传递的源码解析,本篇主要无token 调用过程中,代码的不断完善及其重构过程。 需求很简单如下图,如果资源服务器的提供的接口,客户端不需要身份验证即不需要携带合法令牌也能访问,并且可以实现远程调用的安全性校验。 第一版本 资源服务器设置接口permitall,配置ignore url 即可 ignore-urls: - /actuator/** - /v2/api-docs 保证A对外暴露,A --> B 暴露的服务接口安全 自定义 @Inner 校验逻辑,判断接口请求中是否含有 XX 请求头 /** * @author lengleng * <p> * 服务间接口不鉴权处理逻辑 */ @Slf4j @Aspect @Component @AllArgsConstructor public class PigxSecurityInnerAspect { private final HttpServletRequest requ...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- CentOS7设置SWAP分区,小内存服务器的救世主
- Windows10,CentOS7,CentOS8安装MongoDB4.0.16
- CentOS8安装MyCat,轻松搞定数据库的读写分离、垂直分库、水平分库
- SpringBoot2整合Thymeleaf,官方推荐html解决方案
- 2048小游戏-低调大师作品
- CentOS8编译安装MySQL8.0.19
- Hadoop3单机部署,实现最简伪集群
- Springboot2将连接池hikari替换为druid,体验最强大的数据库连接池
- Windows10,CentOS7,CentOS8安装Nodejs环境
- CentOS7编译安装Gcc9.2.0,解决mysql等软件编译问题