Java线程池ThreadPoolExcutor源码解读详解08-阻塞队列之LinkedBlockingDeque

本文涉及的产品
应用实时监控服务-应用监控,每月50GB免费额度
可观测监控 Prometheus 版,每月50GB免费额度
函数计算FC,每月15万CU 3个月
简介: **摘要:**本文分析了Java中的LinkedBlockingDeque,它是一个基于链表实现的双端阻塞队列,具有并发安全性。LinkedBlockingDeque可以作为有界队列使用,容量由构造函数指定,默认为Integer.MAX_VALUE。队列操作包括在头部和尾部的插入与删除,这些操作由锁和Condition来保证线程安全。例如,`linkFirst()`和`linkLast()`用于在队首和队尾插入元素,而`unlinkFirst()`和`unlinkLast()`则用于删除首尾元素。队列的插入和删除方法根据队列是否满或空,可能会阻塞或唤醒等待的线程,这些操作通过`notFul

💪🏻 制定明确可量化的目标,坚持默默的做事。




一、继承实现关系图

image.gif 1711819665809.png

二、低层数据存储结构

2.1 主要属性

public class LinkedBlockingDeque<E>
    extends AbstractQueue<E>
    implements BlockingDeque<E>, java.io.Serializable {
...
    static final class Node<E> {
        E item;
        Node<E> prev;
        Node<E> next;
        Node(E x) {
            item = x;
        }
    }
    transient Node<E> first;
    transient Node<E> last;
    private transient int count;
    private final int capacity;
    final ReentrantLock lock = new ReentrantLock();
    private final Condition notEmpty = lock.newCondition();
    private final Condition notFull = lock.newCondition();
...
}

image.gif

说明:

  • Node: 基于链表实现的双向链表结构
  • first: 始终指向链表头,本身不存储元素
  • last: 指向链表尾
  • count: 链表元素的数量
  • capacity: 链表的容量
  • lock: 存取对象锁(存取同一把锁)
  • notEmpty: 队列非空阻塞和唤醒条件
  • notFull: 队列是否已满阻塞和唤醒条件

2.2 构造器:

public LinkedBlockingDeque() {
        this(Integer.MAX_VALUE);
    }
    public LinkedBlockingDeque(int capacity) {
        if (capacity <= 0) throw new IllegalArgumentException();
        this.capacity = capacity;
    }
    public LinkedBlockingDeque(Collection<? extends E> c) {
        ...
    }

image.gif

说明:

  • 默认无参构造器,容量为Intege.MAX_VALUE
  • 指定队列容量构造器
  • 初始化元素构造器,容量为Integer.MAX_VALUE

三、特点及优缺点

  1. 双向并发无界阻塞链表
  2. 线程安全
  3. 可从队头或队尾插入元素、可从队头或队尾删除元素
  4. 插入和删除元素是同一把锁,效率不高
  5. 通过ReentrantLock实现锁
  6. 利用Condition实现队列的阻塞等待唤醒

四、源码详解

核心入队方法:

  • offerFirst(E e):添加头节点元素(不阻塞)
  • offerLast(E e):添加尾节点元素(不阻塞)
  • putFirst(E e):添加头节点元素,若添加不成功则阻塞直到被唤醒且添加成功
  • putLast(E e):添加尾节点元素,若添加不成功则阻塞直到被唤醒且添加成功
  • offerFirst(E e, long timeout, TimeUnit unit):添加头节点元素,若添加不成功则阻塞,直到被唤醒且添加成功或超时退出
  • offerLast(E e, long timeout, TimeUnit unit):添加尾节点元素,若添加不成功则阻塞,直到被唤醒且添加成功或超时退出
  • linkFirst(Node<E> node):头部插入(private)
  • linkLast(Node<E> node):尾部插入(private)

       PS:添加头节点最终调linkFirst,添加尾节点最终调linkLast,下面详细阅读这两个方法

4.1 linkFirst(Node<E> node)

private boolean linkFirst(LinkedBlockingDeque.Node<E> node) {
    // 当前容量大于队列最大容量时,返回插入失败
    if (count >= capacity)
        return false;
    // 队列头结点
    LinkedBlockingDeque.Node<E> f = first;
    // 原来的头结点作为 新插入结点的后一个结点
    node.next = f;
    // 替换头结点 为新插入的结点
    first = node;
    // 尾结点不存在,将尾结点置为当前新插入的结点
    if (last == null)
        last = node;
    else
        // 原来的头结点的上一个结点为当前新插入的结点
        f.prev = node;
    // 当前容量加1
    ++count;
    // 唤醒读取时因队列中无元素而导致阻塞的线程
    notEmpty.signal();
    // 返回插入成功信息
    return true;
}

image.gif

4.2 linkLas(Node<E> node)

private boolean linkLast(Node<E> node) {
    // 当前容量大于队列最大容量时,直接返回插入失败
    if (count >= capacity)
        return false;
    // 获取尾节点
    Node<E> l = last;
    // 将新插入的前一个节点指向原来的尾节点
    node.prev = l;
    // 尾结点设置为新插入的结点
    last = node;
    // 若头结点为空,新插入的结点作为头节点
    if (first == null)
        first = node;
    else
        // 将原尾结点的下一个结点指向新插入的节点
        l.next = node;
    // 当前容量加1
    ++count;
    // 唤醒读取时因队列中无元素而导致阻塞的线程
    notEmpty.signal();
    // 返回插入成功信息
    return true;
}

image.gif

核心出队方法:

  • pollFirst():删除头节点(不阻塞)
  • pollLast():删除尾节点(不阻塞)
  • takeFirst():删除头节点,删除不成功则阻塞直到被唤醒且删除成功
  • takeLast():删除尾节点,删除不成功则阻塞直到被唤醒且删除成功
  • pollFirst(long timeout, TimeUnit unit):删除头节点,若删除不成功则阻塞直到被唤醒且删除成功或者超时退出
  • pollLast(long timeout, TimeUnit unit):删除尾节点,若删除不成功则阻塞直到被唤醒且删除成功或者超时退出
  • unlinkFirst():删除头节点(private)
  • unlinkLast():删除尾节点(private)

       PS:添加头节点最终调linkFirst,添加尾节点最终调linkLast,下面详细阅读这两个方法

4.3 unlinkFirst()

// 删除头结点
private E unlinkFirst() {
    // 获取当前头结点
    Node<E> f = first;
    // 若头结点为空,直接返回空
    if (f == null)
        return null;
    // 获取头结点的下一个结点
    Node<E> n = f.next;
    // 获取头结点元素(记录return需要用到的删除了哪个元素)
    E item = f.item;
    // 将头结点元素置为null
    f.item = null;
    // 将头结点的下一个结点指向自己 方便gc
    f.next = f;
    // 设置头结点为原头结点的下一个结点
    first = n;
    // 若原头结点的下一个结点不存在(队列中没有了结点)
    if (n == null)
        // 将尾结点也置为null
        last = null;
    else
        // 新的头结点的前一个结点指向null,因为他已经作为了头结点 所以不需要指向上一个结点
        n.prev = null;
    // 当前数量减1
    --count;
    // 唤醒因添加元素时队列容量满导致阻塞的线程
    notFull.signal();
    // 返回原来的头结点中的元素
    return item;
}

image.gif

4.4 unlinkLast()

// 删除尾结点
private E unlinkLast() {
    // 获取当前尾结点
    Node<E> l = last;
    // 若尾结点不存在,直接返回null
    if (l == null)
        return null;
    // 获取当前尾结点的上一个结点
    Node<E> p = l.prev;
    // 获取当前尾结点中的元素,需要返回记录
    E item = l.item;
    // 将当前尾结点的元素置为null
    l.item = null;
    // 并将当前尾结点的上一个结点指向自己,方便gc
    l.prev = l;
    // 设置新的尾结点,为原来尾结点的前一个结点
    last = p;
    // 若无新的尾结点,头结点置为空(队列中没有了结点)
    if (p == null)
        first = null;
    else
        // 将新的尾结点的下一个结点指向null,因为他已经为尾结点所以不需要指向下一个结点
        p.next = null;
    // 数量减1
    --count;
    // 唤醒因添加元素时队列容量满导致阻塞的线程
    notFull.signal();
    // 返回原来的尾结点元素
    return item;
}

image.gif

五、作用及使用场景

  • 线程池阻塞队列
  • 生产者消费者模式
  • 任务调度FIFO和FILO






相关文章
|
2月前
|
安全 Java 测试技术
Java并行流陷阱:为什么指定线程池可能是个坏主意
本文探讨了Java并行流的使用陷阱,尤其是指定线程池的问题。文章分析了并行流的设计思想,指出了指定线程池的弊端,并提供了使用CompletableFuture等替代方案。同时,介绍了Parallel Collector库在处理阻塞任务时的优势和特点。
|
1月前
|
存储 监控 小程序
Java中的线程池优化实践####
本文深入探讨了Java中线程池的工作原理,分析了常见的线程池类型及其适用场景,并通过实际案例展示了如何根据应用需求进行线程池的优化配置。文章首先介绍了线程池的基本概念和核心参数,随后详细阐述了几种常见的线程池实现(如FixedThreadPool、CachedThreadPool、ScheduledThreadPool等)的特点及使用场景。接着,通过一个电商系统订单处理的实际案例,分析了线程池参数设置不当导致的性能问题,并提出了相应的优化策略。最终,总结了线程池优化的最佳实践,旨在帮助开发者更好地利用Java线程池提升应用性能和稳定性。 ####
|
2月前
|
缓存 监控 Java
Java线程池提交任务流程底层源码与源码解析
【11月更文挑战第30天】嘿,各位技术爱好者们,今天咱们来聊聊Java线程池提交任务的底层源码与源码解析。作为一个资深的Java开发者,我相信你一定对线程池并不陌生。线程池作为并发编程中的一大利器,其重要性不言而喻。今天,我将以对话的方式,带你一步步深入线程池的奥秘,从概述到功能点,再到背景和业务点,最后到底层原理和示例,让你对线程池有一个全新的认识。
57 12
|
29天前
|
监控 Java 开发者
深入理解Java中的线程池实现原理及其性能优化####
本文旨在揭示Java中线程池的核心工作机制,通过剖析其背后的设计思想与实现细节,为读者提供一份详尽的线程池性能优化指南。不同于传统的技术教程,本文将采用一种互动式探索的方式,带领大家从理论到实践,逐步揭开线程池高效管理线程资源的奥秘。无论你是Java并发编程的初学者,还是寻求性能调优技巧的资深开发者,都能在本文中找到有价值的内容。 ####
|
2月前
|
监控 安全 Java
在 Java 中使用线程池监控以及动态调整线程池时需要注意什么?
【10月更文挑战第22天】在进行线程池的监控和动态调整时,要综合考虑多方面的因素,谨慎操作,以确保线程池能够高效、稳定地运行,满足业务的需求。
120 38
|
2月前
|
存储 缓存 监控
Java中的线程池深度解析####
本文深入探讨了Java并发编程中的核心组件——线程池,从其基本概念、工作原理、核心参数解析到应用场景与最佳实践,全方位剖析了线程池在提升应用性能、资源管理和任务调度方面的重要作用。通过实例演示和性能对比,揭示合理配置线程池对于构建高效Java应用的关键意义。 ####
|
2月前
|
Prometheus 监控 Cloud Native
在 Java 中,如何使用线程池监控以及动态调整线程池?
【10月更文挑战第22天】线程池的监控和动态调整是一项重要的任务,需要我们结合具体的应用场景和需求,选择合适的方法和策略,以确保线程池始终处于最优状态,提高系统的性能和稳定性。
351 2
|
缓存 Java
Java之ThreadPoolExcutor和四种常见的线程池(2)
Java之ThreadPoolExcutor和四种常见的线程池(2)
163 0
|
缓存 Java
Java之ThreadPoolExcutor和四种常见的线程池(1)
Java之ThreadPoolExcutor和四种常见的线程池(1)
207 0
|
9天前
|
Java
Java—多线程实现生产消费者
本文介绍了多线程实现生产消费者模式的三个版本。Version1包含四个类:`Producer`(生产者)、`Consumer`(消费者)、`Resource`(公共资源)和`TestMain`(测试类)。通过`synchronized`和`wait/notify`机制控制线程同步,但存在多个生产者或消费者时可能出现多次生产和消费的问题。 Version2将`if`改为`while`,解决了多次生产和消费的问题,但仍可能因`notify()`随机唤醒线程而导致死锁。因此,引入了`notifyAll()`来唤醒所有等待线程,但这会带来性能问题。
Java—多线程实现生产消费者