jdk11源码--LinkedBlockingQueue源码分析

简介: jdk11 LinkedBlockingQueue源码分析

概述

上一篇介绍了jdk11源码--ArrayBlockingQueue源码分析,接下来看一下LinkedBlockingQueue的实现。
这两个阻塞队列最大的区别就是底层元素存储实现不同,ArrayBlockingQueue是基于==数组==,而LinkedBlockingQueue是基于==单向链表==。

LinkedBlockingQueue类图如下:
在这里插入图片描述

LinkedBlockingQueue也是==FIFO==先进先出队列,其实现是 ==双锁队列two lock queue== 算法的变体,它内部维护了一个takeLock和一个putLock,也可以理解为读写锁的一种实现方式。
==思想:锁分离,提高性能。==
下面结合源码具体分析。

构造方法

网上有的说LinkedBlockingQueue是无界队列,其实不太准确,具体看下面的源码。
下面两个构造方法,一个是有参数的,设置容量大小。这是有界队列。
一个是无参数的构造方法,其内部实现是设置容量大小为Integer.MAX_VALUE的队列,这就是网络上说的无界队列,其实还是有界的,只不过比较大,是Integer.MAX_VALUE。

public LinkedBlockingQueue() {
    this(Integer.MAX_VALUE);
}
public LinkedBlockingQueue(int capacity) {
    if (capacity <= 0) throw new IllegalArgumentException();
    this.capacity = capacity;
    last = head = new Node<E>(null);//初始化一个头结点
}

链表节点数据结构

上面说了,LinkedBlockingQueue是单向链表,那么我们看一下他的数据结构,只有一个next指针,典型的单向链表结构:

static class Node<E> {
    E item;
    Node<E> next;
    Node(E x) { item = x; }
}

关键属性

/** 队列容量 */
private final int capacity;
/** 当前队列的元素数量 */
private final AtomicInteger count = new AtomicInteger();
/**
 * 链表的头结点
 * 注意:head.item 永远是 null
 */
transient Node<E> head;
/**
 * 链表的尾结点
 * 注意: last.next 永远是 null
 */
private transient Node<E> last;

/** 读锁: take, poll方法使用 */
private final ReentrantLock takeLock = new ReentrantLock();
/** 读锁的condition */
private final Condition notEmpty = takeLock.newCondition();

/** 写锁: put, offer 方法使用 */
private final ReentrantLock putLock = new ReentrantLock();
/** 写锁的condition */
private final Condition notFull = putLock.newCondition();

count :由于LinkedBlockingQueue使用了两个锁,为了支持线程安全,所以使用原子性的AtomicInteger 来统计队列元素的个数。注意这里count的计数不包含head节点。

put

添加元素至队尾

public void put(E e) throws InterruptedException {
    if (e == null) throw new NullPointerException();
    final int c;
    final Node<E> node = new Node<E>(e);
    final ReentrantLock putLock = this.putLock;
    final AtomicInteger count = this.count;
    putLock.lockInterruptibly();//加写锁,支持中断
    try {
        //如果队列满了,则当前线程阻塞:添加到condition队列中。
        //这里count是线程安全的
        while (count.get() == capacity) {
            notFull.await();
        }
        enqueue(node);//队列不满,写入队尾
        c = count.getAndIncrement();
        if (c + 1 < capacity)//元素入队后,还有剩余空间,就唤醒notFull这个condition队列的第一个线程
            notFull.signal();
    } finally {
        putLock.unlock();
    }
    if (c == 0)//注意这里c是添加元素前的队列长度(getAndIncrement方法先返回值,再将其加1),
        signalNotEmpty();//原队列长度为0,空的,当前put方法添加了一个元素入队,立马唤醒等待在notEmpty condition的 线程来取数据
}

private void signalNotEmpty() {
    final ReentrantLock takeLock = this.takeLock;
    takeLock.lock();
    try {
        notEmpty.signal();//notEmpty是读锁takeLock 的condition队列
    } finally {
        takeLock.unlock();
    }
}

//添加元素至队尾
//enqueue是在写锁内部的,是线程安全的
private void enqueue(Node<E> node) {
    last = last.next = node;
}

这里来看一下if (c + 1 < capacity) notFull.signal();这行代码,为什么这么写。
原因肯定离不开多线程以及condition的原理。
c + 1 < capacity是来判断是否还有剩余空间,当有剩余空间的时候,就唤醒notFull这个condition队列中等待的第一个节点。首先因为是多线程的,所以可能会有多个线程阻塞在notfull上。可能有人会问,前面不是加了putLock的锁吗,这里只能有一个线程进入啊?提出这个疑问的同学请看博主文章jdk11源码--ReentrantLock之Condition源码分析。这里简单说一下:一个线程执行了notFull.await()阻塞后,该线程添加到condition队列中,释放锁,其他线程就可以继续获取锁执行了
当然,这里每次put,空间不满时,都去唤醒一下,确实有肯能会有部分(甚至是大部分)signal操作,这是使用双锁策略及condition的代价,不过这是值得的。

last = last.next = node;再讲一下这句。其实分解一下等价于:

last.next = node;
last = node;

take

从对头取元素

public E take() throws InterruptedException {
    final E x;
    final int c;
    final AtomicInteger count = this.count;
    final ReentrantLock takeLock = this.takeLock;
    takeLock.lockInterruptibly();//加锁,支持中断
    try {
        while (count.get() == 0) {//如果队列为空,则当前线程阻塞在notEmpty condition上
            notEmpty.await();
        }
        x = dequeue();//取走队首元素
        c = count.getAndDecrement();//获取旧的count数量,然后对其减1
        if (c > 1)
            notEmpty.signal();//如果队列中还有值,那么唤醒阻塞在notEmpty condition的线程。
    } finally {
        takeLock.unlock();
    }
    if (c == capacity)//之前队列满的
        signalNotFull();//之前队列满的,那么这里取走了一个,队列有空位子了,马上唤醒notFull condition的一个线程,让其竞争锁put数据
    return x;
}


//返回head节点后面的第一个节点的数据
//并且将head后移一位,老head准备gc回收
private E dequeue() {
    Node<E> h = head;
    Node<E> first = h.next;
    h.next = h; // 辅助GC (为什么不在下面将head设置为null?感觉这里next引用自己,对外也没有引用了,是可以被回收的,效果差不多)
    head = first;
    E x = first.item;
    first.item = null;
    return x;
}
private void signalNotFull() {
    final ReentrantLock putLock = this.putLock;
    putLock.lock();
    try {
        notFull.signal();
    } finally {
        putLock.unlock();
    }
}

总结

两把锁,各配一个condition:

  • takeLock 和 notEmpty 搭配:先获取 takeLock 锁,才能到链表中获取链表第一个节点的数据。如果此时队列为空,则当前线程加入notEmpty 这个condition队列阻塞。
  • putLock 需要和 notFull 搭配:先获取 putLock 锁,才能到链表中插入(put)一个元素到链尾。如果此时队列满,则当前线程加入notFull 这个condition队列阻塞。

唤醒notFull这个condition队列上的线程有两种情况:

  • put、offer每次入队后,还有剩余空间
  • take、poll前队列是满的,那么take走一个后,就唤醒

唤醒notEmpty这个condition队列上的线程有两种情况:

  • take、poll每次取走元素后,队列中不为空
  • put、offer前队列是空的,那么put进来一个元素后,就唤醒

在这里插入图片描述

相关文章
|
20天前
|
XML Java 编译器
Java注解的底层源码剖析与技术认识
Java注解(Annotation)是Java 5引入的一种新特性,它提供了一种在代码中添加元数据(Metadata)的方式。注解本身并不是代码的一部分,它们不会直接影响代码的执行,但可以在编译、类加载和运行时被读取和处理。注解为开发者提供了一种以非侵入性的方式为代码提供额外信息的手段,这些信息可以用于生成文档、编译时检查、运行时处理等。
58 7
|
1月前
|
数据采集 人工智能 Java
Java产科专科电子病历系统源码
产科专科电子病历系统,全结构化设计,实现产科专科电子病历与院内HIS、LIS、PACS信息系统、区域妇幼信息平台的三级互联互通,系统由门诊系统、住院系统、数据统计模块三部分组成,它管理了孕妇从怀孕开始到生产结束42天一系列医院保健服务信息。
32 4
|
1月前
|
监控 Java 应用服务中间件
高级java面试---spring.factories文件的解析源码API机制
【11月更文挑战第20天】Spring Boot是一个用于快速构建基于Spring框架的应用程序的开源框架。它通过自动配置、起步依赖和内嵌服务器等特性,极大地简化了Spring应用的开发和部署过程。本文将深入探讨Spring Boot的背景历史、业务场景、功能点以及底层原理,并通过Java代码手写模拟Spring Boot的启动过程,特别是spring.factories文件的解析源码API机制。
76 2
|
13天前
|
存储 JavaScript 前端开发
基于 SpringBoot 和 Vue 开发校园点餐订餐外卖跑腿Java源码
一个非常实用的校园外卖系统,基于 SpringBoot 和 Vue 的开发。这一系统源于黑马的外卖案例项目 经过站长的进一步改进和优化,提供了更丰富的功能和更高的可用性。 这个项目的架构设计非常有趣。虽然它采用了SpringBoot和Vue的组合,但并不是一个完全分离的项目。 前端视图通过JS的方式引入了Vue和Element UI,既能利用Vue的快速开发优势,
76 13
|
26天前
|
缓存 监控 Java
Java线程池提交任务流程底层源码与源码解析
【11月更文挑战第30天】嘿,各位技术爱好者们,今天咱们来聊聊Java线程池提交任务的底层源码与源码解析。作为一个资深的Java开发者,我相信你一定对线程池并不陌生。线程池作为并发编程中的一大利器,其重要性不言而喻。今天,我将以对话的方式,带你一步步深入线程池的奥秘,从概述到功能点,再到背景和业务点,最后到底层原理和示例,让你对线程池有一个全新的认识。
54 12
|
21天前
|
JavaScript 安全 Java
java版药品不良反应智能监测系统源码,采用SpringBoot、Vue、MySQL技术开发
基于B/S架构,采用Java、SpringBoot、Vue、MySQL等技术自主研发的ADR智能监测系统,适用于三甲医院,支持二次开发。该系统能自动监测全院患者药物不良反应,通过移动端和PC端实时反馈,提升用药安全。系统涵盖规则管理、监测报告、系统管理三大模块,确保精准、高效地处理ADR事件。
|
1月前
|
人工智能 监控 数据可视化
Java智慧工地信息管理平台源码 智慧工地信息化解决方案SaaS源码 支持二次开发
智慧工地系统是依托物联网、互联网、AI、可视化建立的大数据管理平台,是一种全新的管理模式,能够实现劳务管理、安全施工、绿色施工的智能化和互联网化。围绕施工现场管理的人、机、料、法、环五大维度,以及施工过程管理的进度、质量、安全三大体系为基础应用,实现全面高效的工程管理需求,满足工地多角色、多视角的有效监管,实现工程建设管理的降本增效,为监管平台提供数据支撑。
43 3
|
22天前
|
人工智能 移动开发 安全
家政上门系统用户端、阿姨端源码,java家政管理平台源码
家政上门系统基于互联网技术,整合大数据分析、AI算法和现代通信技术,提供便捷高效的家政服务。涵盖保洁、月嫂、烹饪等多元化服务,支持多终端访问,具备智能匹配、在线支付、订单管理等功能,确保服务透明、安全,适用于家庭生活的各种需求场景,推动家政市场规范化发展。
|
1月前
|
运维 自然语言处理 供应链
Java云HIS医院管理系统源码 病案管理、医保业务、门诊、住院、电子病历编辑器
通过门诊的申请,或者直接住院登记,通过”护士工作站“分配患者,完成后,进入医生患者列表,医生对应开具”长期医嘱“和”临时医嘱“,并在电子病历中,记录病情。病人出院时,停止长期医嘱,开具出院医嘱。进入出院审核,审核医嘱与住院通过后,病人结清缴费,完成出院。
98 3
|
1月前
|
JavaScript Java 项目管理
Java毕设学习 基于SpringBoot + Vue 的医院管理系统 持续给大家寻找Java毕设学习项目(附源码)
基于SpringBoot + Vue的医院管理系统,涵盖医院、患者、挂号、药物、检查、病床、排班管理和数据分析等功能。开发工具为IDEA和HBuilder X,环境需配置jdk8、Node.js14、MySQL8。文末提供源码下载链接。