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

简介: **摘要:**本文分析了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






相关文章
|
安全 Java 调度
Netty源码—3.Reactor线程模型二
本文主要介绍了NioEventLoop的执行总体框架、Reactor线程执行一次事件轮询、Reactor线程处理产生IO事件的Channel、Reactor线程处理任务队列之添加任务、Reactor线程处理任务队列之执行任务、NioEventLoop总结。
|
安全 Java
Netty源码—2.Reactor线程模型一
本文主要介绍了关于NioEventLoop的问题整理、理解Reactor线程模型主要分三部分、NioEventLoop的创建和NioEventLoop的启动。
|
机器学习/深度学习 消息中间件 存储
【高薪程序员必看】万字长文拆解Java并发编程!(9-2):并发工具-线程池
🌟 ​大家好,我是摘星!​ 🌟今天为大家带来的是并发编程中的强力并发工具-线程池,废话不多说让我们直接开始。
493 0
|
Java 中间件 调度
【源码】【Java并发】从InheritableThreadLocal和TTL源码的角度来看父子线程传递
本文涉及InheritableThreadLocal和TTL,从源码的角度,分别分析它们是怎么实现父子线程传递的。建议先了解ThreadLocal。
629 4
【源码】【Java并发】从InheritableThreadLocal和TTL源码的角度来看父子线程传递
|
Java 调度
【源码】【Java并发】【线程池】邀请您从0-1阅读ThreadPoolExecutor源码
当我们创建一个`ThreadPoolExecutor`的时候,你是否会好奇🤔,它到底发生了什么?比如:我传的拒绝策略、线程工厂是啥时候被使用的? 核心线程数是个啥?最大线程数和它又有什么关系?线程池,它是怎么调度,我们传入的线程?...不要着急,小手手点上关注、点赞、收藏。主播马上从源码的角度带你们探索神秘线程池的世界...
680 0
【源码】【Java并发】【线程池】邀请您从0-1阅读ThreadPoolExecutor源码
|
存储 监控 Java
【Java并发】【线程池】带你从0-1入门线程池
欢迎来到我的技术博客!我是一名热爱编程的开发者,梦想是编写高端CRUD应用。2025年我正在沉淀中,博客更新速度加快,期待与你一起成长。 线程池是一种复用线程资源的机制,通过预先创建一定数量的线程并管理其生命周期,避免频繁创建/销毁线程带来的性能开销。它解决了线程创建成本高、资源耗尽风险、响应速度慢和任务执行缺乏管理等问题。
858 60
【Java并发】【线程池】带你从0-1入门线程池
|
监控 Java
java异步判断线程池所有任务是否执行完
通过上述步骤,您可以在Java中实现异步判断线程池所有任务是否执行完毕。这种方法使用了 `CompletionService`来监控任务的完成情况,并通过一个独立线程异步检查所有任务的执行状态。这种设计不仅简洁高效,还能确保在大量任务处理时程序的稳定性和可维护性。希望本文能为您的开发工作提供实用的指导和帮助。
595 17
|
缓存 Java
Java之ThreadPoolExcutor和四种常见的线程池(2)
Java之ThreadPoolExcutor和四种常见的线程池(2)
347 0
|
缓存 Java
Java之ThreadPoolExcutor和四种常见的线程池(1)
Java之ThreadPoolExcutor和四种常见的线程池(1)
434 0
|
11月前
|
JSON 网络协议 安全
【Java】(10)进程与线程的关系、Tread类;讲解基本线程安全、网络编程内容;JSON序列化与反序列化
几乎所有的操作系统都支持进程的概念,进程是处于运行过程中的程序,并且具有一定的独立功能,进程是系统进行资源分配和调度的一个独立单位一般而言,进程包含如下三个特征。独立性动态性并发性。
543 1

热门文章

最新文章