并发编程——12 阻塞队列BlockingQueue实战及其原理分析

1 阻塞队列介绍

1.1 队列

  • 队列是一种特殊的线性表,具有以下核心特点:

    • 操作限制:仅允许在一端(队尾)插入元素,在另一端(队头)删除元素

    • 先进先出(FIFO):先入队的元素会先出队,类似现实中的排队场景(如入队顺序是 1、2、3、4,出队顺序也为 1、2、3、4);

    • 两端定义:允许入队的一端叫队尾,允许出队的一端叫队头

  • Queue 是 Java 集合框架中的接口(继承自 Collection),定义了队列的基本操作,不同方法的行为差异主要体现在“队列满/空时是否抛异常”:

    方法功能描述队列满/空时的行为
    add(E e)向队尾添加元素,添加成功返回 true队列满时抛出 IllegalStateException 异常
    offer(E e)向队尾添加元素,添加成功返回 true队列满时返回 false,不抛异常
    remove()返回并删除队头元素队列为空时抛出 NoSuchElementException 异常
    poll()返回并删除队头元素队列为空时返回 null,不抛异常
    element()返回队头元素,但不删除队列为空时抛出 NoSuchElementException 异常
    peek()返回队头元素,但不删除队列为空时返回 null,不抛异常
    public interface Queue<E> extends Collection<E> {
        // 向队尾添加一个元素,添加成功返回true, 如果队列满了,就会抛出异常
        boolean add(E e);
        // 向队尾添加一个元素,添加成功返回true, 如果队列满了,返回false,不抛异常
        boolean offer(E e);
        // 返回并删除队首元素,队列为空则抛出异常
        E remove();
        // 返回并删除队首元素,队列为空则返回null
        E poll();
        // 返回队首元素,但不移除,队列为空则抛出异常
        E element();
        // 获取队首元素,但不移除,队列为空则返回null
        E peek();
    }
    

1.2 阻塞队列

  • 阻塞队列(BlockingQueue)是 java.util.concurrent 包下的线程安全数据结构,核心特性是**“阻塞式”的线程同步**:

    • 入队阻塞:如果队列已满,入队线程会被阻塞,直到队列有空间可用;

    • 出队阻塞:如果队列已空,出队线程会被阻塞,直到队列有元素可用;

  • BlockingQueue 继承自 Queue 接口,在其基础上扩展了阻塞式的入队/出队方法,并按“行为风格”分为四类:

    方法风格入队方法出队方法获取队首元素方法
    抛出异常add(e)remove()element()
    返回特定值offer(e)poll()peek()
    阻塞(无超时)put(e)take()不支持
    阻塞(带超时)offer(e, time, unit)poll(time, unit)不支持
    • put(e) / take():是阻塞队列的核心方法。put 会在队列满时阻塞线程,直到有空间;take 会在队列空时阻塞线程,直到有元素。适用于必须等待队列可用的强同步场景(如线程池任务队列);

    • offer(e, time, unit) / poll(time, unit):带超时的阻塞方法。若在指定时间内队列仍不可用,会放弃操作并返回结果(offer 返回 falsepoll 返回 null)。适用于“可容忍等待超时”的场景;

    • add / remove / element:与普通 Queue 行为一致,队列满/空时抛异常,一般用于队列容量确定、不允许满/空的场景;

    • offer / poll / peek:队列满/空时返回特定值(false / null),适用于无需阻塞、温和处理满/空的场景。

1.3 应用场景

  • 线程池

    • 线程池的任务队列是阻塞队列。当提交的任务数超过线程池的处理能力时,新任务会进入阻塞队列等待;若队列为空,工作线程会被阻塞,直到有新任务提交;

    • 通过阻塞式入队/出队实现任务的有序调度和线程同步,避免了手动处理线程等待、唤醒的复杂度,保证线程池的稳定运行;

  • 生产者-消费者模型

    • 生产者线程向阻塞队列中生产元素,消费者线程从队列中消费元素。若队列满,生产者阻塞;若队列空,消费者阻塞;

    • 完美解决了生产者和消费者之间的并发协作问题,无需手动加锁即可实现“生产-消费”的有序流程,避免线程竞争和数据冲突;

  • 消息队列

    • 生产者将消息放入阻塞队列,消费者从队列中取出消息处理;

    • 实现异步通信和解耦——生产者无需等待消费者处理即可继续生产,不同组件通过队列间接交互;同时利用阻塞特性保证消息的有序处理,提升系统吞吐量和响应性能;

  • 缓存系统

    • 缓存数据更新时,将新数据放入阻塞队列;其他线程从队列中获取最新数据使用;

    • 避免了并发更新缓存时的竞争冲突,保证多线程对缓存数据的“读取-更新”操作有序且安全;

  • 并发任务处理

    • 将待处理任务放入阻塞队列,多个工作线程从队列中取任务并行处理;

    • 避免多个线程重复处理同一任务,同时实现“任务提交”与“任务执行”的解耦,提升系统的可维护性和可扩展性。

1.4 JUC包下的阻塞队列

  • BlockingQueue接口的实现类都被放在了 JUC 包中,它们的区别主要体现在存储结构上或对元素操作上的不同,但是对于takeput操作的原理却是类似的;
阻塞队列实现类存储结构核心特性适用场景
ArrayBlockingQueue数组有界阻塞队列,容量初始化后不可变;支持公平/非公平锁选择适用于“队列容量明确、需严格控制内存”的场景,如线程池的有界任务队列
LinkedBlockingQueue链表默认无界(容量可指定为有界);基于链表的节点动态扩容,插入/删除性能较好适用于“任务数量不确定、需高吞吐量”的场景,如大多数线程池的默认任务队列
PriorityBlockingQueue优先级堆无界阻塞队列,元素按优先级排序(需实现 Comparable 或指定比较器)适用于“任务需按优先级执行”的场景,如调度系统的优先级任务队列
DelayQueue优先级堆(基于 PriorityBlockingQueue无界阻塞队列,元素需实现 Delayed 接口,按延迟时间排序,延迟到期后才可取出适用于“定时任务、延迟消息”的场景,如定时任务调度、延迟消息队列
SynchronousQueue无(不存储元素)特殊阻塞队列,入队和出队操作必须成对出现(生产者入队时需等待消费者出队,反之亦然)适用于“线程间直接传递数据、无缓冲”的场景,如 Executors.newCachedThreadPool() 的任务队列
LinkedTransferQueue链表无界阻塞队列,支持“ transfer ”操作(生产者可等待消费者接收元素后再返回),并发性能优于 LinkedBlockingQueue适用于“需确认消费者接收元素”的高并发场景,如分布式通信中的消息确认
LinkedBlockingDeque链表(双端)双端阻塞队列,支持从队头或队尾入队/出队,是 Deque 接口的阻塞实现适用于“需双端操作”的场景,如工作窃取线程池(Fork/Join Pool)的任务队列

在这里插入图片描述

2 ArrayBlockingQueue

2.1 简介

  • ArrayBlockingQueue 基于数组实现,是有界阻塞队列(初始化时必须指定容量,且容量不可变)。通过 ReentrantLock 实现线程安全,保证多线程下的入队、出队操作原子性;
  • 适用场景:
    • 可用于数据缓存、限流、生产者-消费者模型等场景;
    • 在生产者-消费者模型中:
      • 生产速度与消费速度基本匹配ArrayBlockingQueue 是不错的选择,能稳定承载数据流转;
      • 生产速度远大于消费速度,会导致队列快速填满,大量生产者线程被阻塞,此时需考虑调整消费能力或选择其他队列(如无界队列)。

2.2 使用

// 创建了一个容量为 1024 的 ArrayBlockingQueue 实例 queue
BlockingQueue queue = new ArrayBlockingQueue(1024);
// 通过 put("1") 方法向队列中添加元素 “1”,put 方法是阻塞式入队,如果队列满了会阻塞线程直到队列有空间
queue.put("1");
// 通过 take() 方法从队列中取出元素,take 方法是阻塞式出队,如果队列空了会阻塞线程直到队列中有元素
Object object = queue.take();

2.3 原理及源码

2.3.1 概述

在这里插入图片描述

  • 数据结构:基于静态数组实现,容量固定且必须初始化时指定,无扩容机制,即使位置无元素也会被 null 占位;
  • 线程安全与锁:通过 ReentrantLock 实现线程安全,但入队和出队操作共用同一把锁,导致存取相互排斥,生产者和消费者无法并行操作,高并发场景下性能会有瓶颈;
  • 阻塞机制:依靠 notEmpty(出队时队列空则阻塞)和 notFull(入队时队列满则阻塞)两个对象实现阻塞;
  • 入队出队逻辑:入队从队首开始,记录 putIndex,队尾时置为 0,入队后唤醒 notEmpty;出队从队首开始,记录 takeIndex,队尾时置为 0,出队后唤醒 notFull。两个指针从队首向队尾移动,保证先进先出(FIFO)。

2.3.2 数据结构

  • ArrayBlockingQueue 采用一把锁 + 两个条件的设计:

    • 锁保证了入队、出队操作的互斥性;
    • 两个 Condition 分别处理“队空阻塞消费者”和“队满阻塞生产者”的场景,从而实现了阻塞队列的核心特性;
    // 数据元素数组:用于存储队列元素的静态数组,容量固定(初始化时指定)
    final Object[] items;
    
    // 下一个待取出元素索引
    in t takeIndex;
    
    // 下一个待添加元素索引
    int putIndex;
    
    // 记录队列中当前元素的个数
    int count;
    
    // 内部锁:用于保证多线程下操作的原子性。支持“公平锁”和“非公平锁”(通过构造函数参数fair控制)
    final ReentrantLock lock;
    
    // 消费者:当队列空时,消费者线程会在notEmpty上阻塞,直到有元素被放入
    private final Condition notEmpty;
    
    // 生产者:当队列满时,生产者线程会在notFull上阻塞,直到有元素被取出
    private final Condition notFull;  
    
    public ArrayBlockingQueue(int capacity) {
        this(capacity, false);
    }
    
    public ArrayBlockingQueue(int capacity, boolean fair) {
        ...
        lock = new ReentrantLock(fair); // 公平锁/非公平锁
        notEmpty = lock.newCondition();
        notFull =  lock.newCondition();
    }
    

2.3.3 入队put方法

  • put 方法是 ArrayBlockingQueue 实现阻塞入队的核心逻辑

    public void put(E e) throws InterruptedException {
    	// 先检查元素是否为空
        checkNotNull(e);
        // 加锁
        final ReentrantLock lock = this.lock;
        // 在获取锁的过程中,如果线程中断,抛出异常
        lock.lockInterruptibly();
        try {
           // 如果队列已满(用while不用if是为了防止虚假唤醒)
            while (count == items.length)
                notFull.await(); // 队列满了,使用notFull等待(生产者阻塞)
            // 队列未满,元素入队
            enqueue(e);
        } finally {
            lock.unlock(); // 释放锁
        }
    }
    
    // 元素入队逻辑
    private void enqueue(E x) {
        final Object[] items = this.items;
        // 将元素放入 putIndex 指向的数组位置(putIndex,即下一个待添加元素索引)
        items[putIndex] = x;
        // 添加元素后,putIndex后移。若 putIndex 到达数组末尾,将其置为 0(实现环形数组的逻辑,避免数组复制)
        if (++putIndex == items.length) 
            putIndex = 0
        // 元素计数 count 加 1
        count++;
        // 唤醒阻塞的消费者线程
        notEmpty.signal();
    }
    

    虚假唤醒(False Wakeup)是并发编程中的一个真实存在的现象,它指的是一个等待线程在没有被其他线程通过 notifynotifyAll 或条件满足的情况下被唤醒;

    ArrayBlockingQueueput 方法为例:

    • 当队列满时,生产者线程确实会在 notFull 条件队列上等待:notFull.await()
    • 如果使用 if 判断,线程被唤醒后(无论是因为其他消费者take后调用了notFull.signal(),还是发生了虚假唤醒),它会直接执行 if 之后的代码,认为条件已经满足(队列不满),从而执行入队操作;
    • 但此时队列可能仍然是满的(如果唤醒是虚假的,或者有多个生产者线程被唤醒但只有一个成功操作),这将导致count超过items.length,破坏数据结构的不变性,造成严重的数据错误(如数组越界);

    while循环提供了一个自旋检查的机制:

    • 线程被唤醒后,第一件事不是盲目地继续执行,而是重新检查它正在等待的条件count == items.length);
    • 如果条件仍然为真(队列还是满的),它会再次调用 await() 让自己阻塞。这就像在说:“我醒了,但我再确认一下我是不是真的该醒了。”;
    • 只有当他被唤醒并且条件检查为假(队列确实不满了),它才会退出循环,继续执行后续的入队操作;
  • 思考: 为什么 ArrayBlockingQueue 对数组操作要设计成双指针?

    在这里插入图片描述

    • ArrayBlockingQueue 采用双指针(takeIndex 记录出队位置,putIndex 记录入队位置)的核心目的是优化性能

      • 若使用单指针,每次出队后需要将后面的元素全部前移,入队时需要将后面的元素全部后移,时间复杂度为 O(n),性能极差;

      • 双指针 + 环形数组的设计,让入队和出队操作可以直接在指针指向的位置进行,无需移动其他元素,时间复杂度降为 O(1),大幅提升了队列的插入和删除性能。

2.3.4 出队take方法

  • take 方法是 ArrayBlockingQueue 实现阻塞出队的核心逻辑,步骤如下:

    public E take() throws InterruptedException {
        // 加锁
        final ReentrantLock lock = this.lock;
        // 在获取锁的过程中,如果线程中断,抛出异常
        lock.lockInterruptibly();
        try {
           //如果队列为空,则消费者挂起
            while (count == 0)
                notEmpty.await();
            // 队列元素出队
            return dequeue();
        } finally {
            lock.unlock(); // 释放锁
        }
    }
    private E dequeue() {
        final Object[] items = this.items;
        @SuppressWarnings("unchecked")
        // 从 takeIndex 指向的数组位置取出元素(takeIndex,即下一个待取出元素索引)
        E x = (E) items[takeIndex];
        // 要出队的元素被取出后,将其所在位置置为空
        items[takeIndex] = null;
        // 元素出队后,takeIndex后移。若 takeIndex 到达数组末尾,将其置为 0(实现环形数组,避免数组复制)
        if (++takeIndex == items.length)
            takeIndex = 0;
        // 元素计数 count 减 1
        count--;
        if (itrs != null)
            itrs.elementDequeued();
        // 唤醒生产者线程
        notFull.signal();
        return x;
    }
    

3 LinkedBlockingQueue

3.1 简介

  • LinkedBlockingQueue 是一个基于链表实现的阻塞队列,默认情况下,该阻塞队列的大小为Integer.MAX_VALUE(数值极大),因此被称为无界队列,队列可随元素添加动态增长;但如果没有剩余内存,会抛出 OOM(内存溢出) 错误;
  • 为避免队列过大导致机器负载过高或内存爆满,建议在使用时手动指定队列大小

3.2 使用

// 创建了一个有界队列,指定容量为 100
BlockingQueue<Integer> boundedQueue = new LinkedBlockingQueue<>(100);
// 创建了一个无界队列,默认容量为 Integer.MAX_VALUE,队列会随元素添加动态增长,但存在内存溢出(OOM)风险
BlockingQueue<Integer> unboundedQueue = new LinkedBlockingQueue<>();

3.3 原理及源码

3.3.1 概述

在这里插入图片描述

  • 存储结构:内部由单链表实现,元素从 head(队首)取出,从 tail(队尾)添加,遵循先进先出(FIFO)规则;
  • 锁分离技术:采用两把独立的锁takeLock 用于出队操作,putLock 用于入队操作),实现了读写分离——入队和出队操作可以并行执行,互不阻塞,相比 ArrayBlockingQueue 的单锁设计,并发性能更优;
  • 阻塞机制:通过 notEmpty(队空时阻塞消费者)和 notFull(队满时阻塞生产者)两个条件变量实现阻塞控制;
  • 容量特性:默认是无界队列(容量为 Integer.MAX_VALUE),也可手动指定容量变为有界队列;但无界队列存在内存溢出(OOM)风险,实际使用建议指定容量。

3.3.2 数据结构

  • LinkedBlockingQueue 采用两把锁 + 两个条件变量 + 单链表的设计:

    • 锁分离技术让入队、出队操作并行执行,解决了 ArrayBlockingQueue 单锁导致的并发瓶颈;
    • 原子计数 count 保证了元素数量的线程安全统计;
    • 单链表结构支持动态扩容(无界场景下),同时通过 capacity 可灵活控制队列大小;
    // 队列的容量,指定后队列变为有界队列;未指定时默认是 Integer.MAX_VALUE(无界)
    private final int capacity;
    
    // 原子性地记录队列中元素的数量,保证多线程下计数的准确性
    private final AtomicInteger count = new AtomicInteger();
    
    // 链表的头节点,head 初始化时不存储元素
    transient Node<E> head;
    
    // 单链表的节点结构,包含存储元素的 item 和指向下一节点的 next 指针
    private transient Node<E> last;
    
    // 两把独立的 ReentrantLock,分别控制 “出队操作” 和 “入队操作”,实现锁分离——入队和出队可以并行执行,互不阻塞,大幅提升并发性能
    private final ReentrantLock takeLock = new ReentrantLock(); // take锁
    
    private final ReentrantLock putLock = new ReentrantLock(); // put锁
    
    // 消费者线程在 takeLock 加锁后,若队空则阻塞在 notEmpty 上
    private final Condition notEmpty = takeLock.newCondition();
    
    // 生产者线程在 putLock 加锁后,若队满则阻塞在 notFull 上
    private final Condition notFull = putLock.newCondition();
    
    //典型的单链表结构
    static class Node<E> {
        E item;  // 存储元素
        Node<E> next;  // 指向下一节点的 next 指针
        Node(E x) { item = x; }
    }
    

3.3.3 构造器

// 无参构造器。会调用有参构造器,默认将容量设为 Integer.MAX_VALUE(极大值,即无界队列)
public LinkedBlockingQueue() {
    this(Integer.MAX_VALUE);
}

// 有参构造器。会校验容量(必须大于 0),然后初始化队列容量,并将 head 和 last 指针指向一个空节点,完成队列的初始化
public LinkedBlockingQueue(int capacity) {
    if (capacity <= 0) throw new IllegalArgumentException();
    this.capacity = capacity;
    // 初始化head和last指针为空值节点
    last = head = new Node<E>(null);
}

3.3.4 入队put方法

  • put 方法是 LinkedBlockingQueue 实现阻塞入队的关键逻辑,步骤如下:

    public void put(E e) throws InterruptedException {
        
        // 不允许元素为 null,否则抛出 NullPointerException
        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;
        // 通过 putLock 加锁(支持线程中断,调用 lockInterruptibly)
        putLock.lockInterruptibly();
        try {
            // 如果队列已满
            while (count.get() == capacity) {
                notFull.await(); // 让生产者线程阻塞,直到队列有空间(用 while 防止 “虚假唤醒”)
            }  
            // 如果队列未满,调用 enqueue 方法将新节点添加到链表尾部(last 指针更新)
            enqueue(node);
            c = count.getAndIncrement(); // 原子计数 count 加 1
            // 若队列仍未满(c + 1 < capacity),唤醒其他阻塞在 notFull 上的生产者线程;
            if (c + 1 < capacity)
                notFull.signal();
        } finally {
            putLock.unlock(); // 释放锁
        }
        // 若原队列长度为 0(c == 0),现在加了一个元素后,调用 signalNotEmpty 唤醒阻塞在 notEmpty 上的消费者线程
        if (c == 0)
            signalNotEmpty();
    }
    
    // 将新节点添加到 last 指针之后,更新 last 指向新节点,实现链表的尾部插入
    private void enqueue(Node<E> node) { 
        last = last.next = node;
    }
    
    // 获取 takeLock 后,唤醒阻塞在 notEmpty 上的消费者线程,通知队列已有元素可消费
    private void signalNotEmpty() {
        final ReentrantLock takeLock = this.takeLock; 
        takeLock.lock();
        try {  
            notEmpty.signal();
        } finally {
            takeLock.unlock();
        }
    }
    

3.3.5 出队take方法

  • take 方法是 LinkedBlockingQueue 实现阻塞出队的关键逻辑:

    public E take() throws InterruptedException {
        E x;
        int c = -1;
        final AtomicInteger count = this.count;
        final ReentrantLock takeLock = this.takeLock;
        // 通过 takeLock 加锁(支持线程中断,调用 lockInterruptibly)
        takeLock.lockInterruptibly();
        try {
            // 如果队列为空,让消费者线程阻塞,直到队列有元素(用 while 防止 “虚假唤醒”)
            while (count.get() == 0) {
                notEmpty.await();
            }
            // 调用 dequeue 方法从链表头部取出元素(更新 head 指针)
            x = dequeue();
            c = count.getAndDecrement(); // 原子计数 count 减 1
            // 若原队列长度大于 1(c > 1),唤醒其他阻塞在 notEmpty 上的消费者线程
            if (c > 1)
                notEmpty.signal();
        } finally {
            takeLock.unlock(); // 真正唤醒消费者线程
        }
        // 若原队列长度等于容量(c == capacity,即出队后队列由满变空),调用 signalNotFull 唤醒阻塞在 notFull 上的生产者线程
        if (c == capacity)
            signalNotFull();
        return x;
    }
    
    private E dequeue() {
        // head 节点本身不存储元素
        Node<E> h = head;
        Node<E> first = h.next; // 取出 head.next 节点的元素
        h.next = h; // 将该节点置为新 head 并清空其值,同时让原 head 节点的引用断开(方便 GC 回收)
        head = first;
        E x = first.item;
        first.item = null;
        return x;
    }
    
    // 获取 putLock 后,唤醒阻塞在 notFull 上的生产者线程,通知队列已有空间可入队
    private void signalNotFull() {
        final ReentrantLock putLock = this.putLock;
        putLock.lock();
        try {
            notFull.signal();
        } finally {
            putLock.unlock();
        }
    }
    
  • 为什么队列是满的才唤醒阻塞在notFull上的线程呢?

    • LinkedBlockingQueue 采用两把独立的锁takeLock 控制出队,putLock 控制入队)实现锁分离,让入队和出队可并行执行。但这种设计也带来了一个问题:唤醒 notFull 条件的线程需要获取 putLock
    • 若每次出队后都去尝试唤醒 notFull,会频繁地获取和释放 putLock,增加锁竞争的开销;
    • 因此,设计上选择仅当队列从“满”变为 “非满” 时(即 c == capacity 时)才唤醒 notFull 上的线程。这样可以减少 putLock 的竞争次数,平衡 “锁分离的性能优势” 和 “唤醒操作的开销”,是一种 trade-off(取舍)的优化策略。

3.4 LinkedBlockingQueue VS ArrayBlockingQueue

对比维度ArrayBlockingQueueLinkedBlockingQueue
队列大小有界,初始化必须指定容量可有界(指定容量)或无界(默认 Integer.MAX_VALUE);无界时若生产速度远大于消费速度,可能引发内存溢出(OOM)
存储结构基于数组存储基于链表(Node 节点)存储
对象创建开销插入/删除元素时无额外对象创建每次入队会生成新的 Node 对象,长期高吞吐场景下可能增加 GC 压力
锁机制单锁(ReentrantLock),入队和出队操作互斥锁分离(putLock 控制入队,takeLock 控制出队),入队和出队可并行执行,高并发下吞吐量更优

4 SynchronousQueue

4.1 简介

  • SynchronousQueue是一个容量为0的阻塞队列,没有数据缓冲。生产者线程执行 put 操作时,必须等待消费者线程执行 take 操作,才能完成元素的传递(直接从生产者“移交”给消费者,无中间存储);

  • 阻塞逻辑

    • 消费者取数据时,若队列无元素(容量为0,天然无元素),会阻塞直到生产者放入数据;
    • 生产者放数据时,若无消费者等待取数据,会阻塞直到有消费者来取;

    在这里插入图片描述

4.2 应用场景

  • 传递性场景:非常适合生产者和消费者线程同步传递信息、事件或任务的场景,实现两者的直接交互;
  • 线程池场景:在线程池中(如 Executors.newCachedThreadPool()),当不确定生产者请求数量但需要快速处理时,SynchronousQueue 能为每个生产者请求分配一个消费线程,达到最高处理效率。该线程池会根据新任务创建线程,空闲线程可重复使用,空闲60秒后会被回收。

4.3 使用

  • 创建方式:

    # 构建同步队列
    SynchronousQueue<Integer> queue = new SynchronousQueue<>();
    
  • 使用SynchronousQueue时需格外谨慎,因为它极易导致死锁。如果生产者和消费者线程的设计不同步,会造成程序无法继续执行,因此必须做好线程同步和错误处理。死锁演示:

    public class SynchronousQueueDeadlockDemo {
        public static void main(String[] args) {
            final SynchronousQueue<Integer> queue = new SynchronousQueue<>();
    
            Thread thread1 = new Thread(() -> {
                try {
                    // 线程1尝试将数据放入队列
                    int data = 42;
                    queue.put(data);
                    System.out.println("线程1放入数据:" + data);
    
                    // 接着,线程1尝试从队列中获取数据,但此时没有其他线程来获取
                    int result = queue.take();
                    System.out.println("线程1获取数据:" + result);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
    
            Thread thread2 = new Thread(() -> {
                try {
    
                    // 接着,线程2尝试将数据放入队列,但此时没有其他线程来获取
                    int result = 100;
                    queue.put(result);
                    System.out.println("线程2放入数据:" + result);
    
                    // 线程2尝试从队列中获取数据,但此时没有数据可用
                    int data = queue.take();
                    System.out.println("线程2获取数据:" + data);
    
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
    
            thread1.start();
            thread2.start();
        }
    }
    
    • 线程1:先执行queue.put(42)放入数据,但后续又执行queue.take()尝试取数据。此时没有其他线程来消费它放入的数据,所以 take 操作会阻塞;

    • 线程2:先执行 queue.put(100) 放入数据,后续执行 queue.take() 尝试取数据。同样,没有其他线程消费它放入的数据,take 操作也会阻塞。

    • 死锁结果:两个线程都因“等待对方或其他线程消费自己的元素”而陷入永久阻塞,程序无法继续执行。

5 PriorityBlockingQueue

5.1 简介

  • PriorityBlockingQueue 是一个无界的基于数组的优先级阻塞队列,数组默认长度为11,可无限扩容(直到资源耗尽)。

    • 每次出队返回**优先级最高(或最低)**的元素;
    • 默认按元素的自然顺序升序排列;也可通过构造函数指定 Comparator 自定义排序规则;
    • 注意:无法保证同优先级元素的出队顺序

    在这里插入图片描述

5.2 应用场景

  • 电商抢购活动,会员级别高的用户优先抢购到商品;
  • 银行办理业务,VIP 客户插队。

5.3 使用

// 创建了一个初始容量为 5 的优先级阻塞队列,Comparator 为 null,采用元素的自然排序(例如整数默认升序)
PriorityBlockingQueue<Integer> queue=new PriorityBlockingQueue<Integer>(5);

// 通过自定义 Comparator 创建队列,这里的 Comparator 实现了“o2 - o1”的逻辑,即降序排序(优先级高的元素先出队),同时指定初始容量为 5
PriorityBlockingQueue queue=new PriorityBlockingQueue<Integer>(5, new Comparator<Integer>() {
    @Override
    public int compare(Integer o1, Integer o2) {
        return o2-o1;
    }
});

5.4 优先级队列的三种构造方式

  • 使用普通线性数组(无序)实现优先级队列

    在这里插入图片描述

    • 执行插入操作时,直接插入到数组末端,时间复杂度为 O(1)

    • 获取优先级最高元素,需遍历整个数组匹配,时间复杂度为 O(n)

    • 删除优先级最高元素:先查找(O(n))再移动元素填补空缺(O(n)),总时间复杂度为 O(n)

    • 缺点:获取和删除优先级最高元素的效率较低;

  • 使用一个按顺序排列的有序向量实现优先级队列

    在这里插入图片描述

    • 获取优先级最高元素:直接取队首,时间复杂度为 O(1)

    • 删除优先级最高元素:直接删除队首,时间复杂度为 O(1)

    • 插入操作:需先二分查找插入位置(O(logn)),再移动后续元素(O(n)),总时间复杂度为 O(n)

    • 缺点:插入操作效率较低;

  • 二叉堆

    • 完全二叉树:除最后一层外,其他层节点全满;最后一层的叶子节点从左到右排列;

    • 二叉堆分类

      • 大顶堆(最大堆):父节点的值大于等于所有子节点的值,堆顶(根节点)是优先级最高的元素;
      • 小顶堆(最小堆):父节点的值小于等于所有子节点的值,堆顶(根节点)是优先级最低的元素;

      在这里插入图片描述

    • 优势:插入、获取/删除优先级最高元素的时间复杂度可优化至 O(logn),是优先级队列的高效实现方式(如 PriorityBlockingQueue 底层基于二叉堆)。

6 DelayQueue

6.1 简介

  • 队列功能:是一个支持延迟获取元素的阻塞队列,内部基于优先级队列(PriorityQueue)存储元素。元素必须实现 Delayed 接口,在创建时指定延迟时间,只有延迟期满后才能从队列中提取元素;

  • 排序规则:不是先进先出(FIFO),而是按延迟时间长短排序——延迟时间越短(越先到期)的元素排在队列最前面,下一个即将执行的任务会优先被取出;

  • 队列属性:是无界队列,放入的元素需实现 Delayed 接口;Delayed 接口继承了 Comparable 接口,因此元素天然具备比较和排序能力;

  • Delayed 接口关键方法

    public interface Delayed extends Comparable<Delayed> {
        // 用于返回 “元素还剩多长时间才会到期(可被提取)”,返回 0 或负数表示任务已过期
        long getDelay(TimeUnit unit);
    }
    

6.2 使用

  • 下面通过 DelayQueue 实现“延迟订单处理”——订单按设定的延迟时间到期后,才会被从队列中取出并处理。核心是让 Order 类实现 Delayed 接口,定义延迟时间和排序规则;

    public class DelayQueueExample {
    
        public static void main(String[] args) throws InterruptedException {
            
            // 创建 DelayQueue<Order> 实例,添加三个延迟时间不同的订单(分别延迟 5 秒、2 秒、3 秒)
            DelayQueue<Order> delayQueue = new DelayQueue<>();
            delayQueue.put(new Order("order1", System.currentTimeMillis(), 5000));
            delayQueue.put(new Order("order2", System.currentTimeMillis(), 2000));
            delayQueue.put(new Order("order3", System.currentTimeMillis(), 3000));
    
            // 通过 take 方法循环取出订单(take 会阻塞直到订单到期),并打印处理结果
            while (!delayQueue.isEmpty()) {
                Order order = delayQueue.take();
                System.out.println("处理订单:" + order.getOrderId());
            }
        }
    
        static class  Order implements Delayed{
            // 包含订单编号(orderId)、创建时间(createTime)、延迟时间(delayTime)等属性
            private String orderId;
            private long createTime;
            private long delayTime;
    
            public Order(String orderId, long createTime, long delayTime) {
                this.orderId = orderId;
                this.createTime = createTime;
                this.delayTime = delayTime;
            }
    
            public String getOrderId() {
                return orderId;
            }
    
            // 计算订单剩余延迟时间(创建时间 + 延迟时间 - 当前时间),返回值为 0 或负数时表示订单已到期
            @Override
            public long getDelay(TimeUnit unit) {
                long diff = createTime + delayTime - System.currentTimeMillis();
                return unit.convert(diff, TimeUnit.MILLISECONDS);
            }
    
            // 根据剩余延迟时间比较订单优先级,延迟时间越短(越先到期)的订单排序越靠前
            @Override
            public int compareTo(Delayed o) {
                long diff = this.getDelay(TimeUnit.MILLISECONDS) - o.getDelay(TimeUnit.MILLISECONDS);
                return Long.compare(diff, 0);
            }
        }
    }
    
  • 执行结果与原理:这是因为 DelayQueue 会按延迟时间从短到长的顺序取出元素(order2 延迟2秒最早到期,order1 延迟5秒最晚到期)

    在这里插入图片描述

6.3 原理及源码

在这里插入图片描述

6.3.1 数据结构

// 用于保证队列操作的线程安全,对入队、出队等操作进行加锁控制
private final transient ReentrantLock lock = new ReentrantLock();

// 优先级队列,用于存储元素并保证延迟时间短的元素优先执行(即按延迟时间排序,最早到期的元素在队首)
private final PriorityQueue<E> q = new PriorityQueue<E>();

// 标记当前是否有线程在排队取元素,指向 “第一个从队列获取元素时阻塞的线程”,用于优化多线程竞争场景
private Thread leader = null;

// 基于 lock 创建的条件变量,用于表示 “是否有可取出的元素”;当新元素到达或线程需要成为 leader 时,通过该条件变量进行通知
private final Condition available = lock.newCondition();

// 无参构造器:创建空的延迟队列
public DelayQueue() {}

// 有参构造器:将集合 c 中的元素批量添加到队列中,通过 addAll 方法完成初始化
public DelayQueue(Collection<? extends E> c) {
    this.addAll(c);
}

6.3.2 入队put方法

  • put 方法直接调用 offer 方法,offerDelayQueue 入队操作的核心实现:

    public void put(E e) {
        offer(e);
    }
    
    public boolean offer(E e) {
        // 通过 ReentrantLock 加锁,保证入队操作的线程安全
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            // 将元素 e 放入内部的 PriorityQueue(q)中,PriorityQueue 会根据元素的延迟时间自动排序(延迟时间越短,排序越靠前)
            q.offer(e);
            // 若入队的元素 e 是队列的头部元素,说明它是当前延迟时间最短、最优先到期的元素
            if (q.peek() == e) {
                // 将 leader 置空(表示没有线程在排队等待取元素,或需要重新确定 leader 线程)
                leader = null;
                // 唤醒阻塞在 available 条件变量上的线程(通知它们有可取出的元素了)
                available.signal();
            }
            return true;
        } finally {
            lock.unlock(); // 释放锁
        }
    }
    
  • DelayQueue 的入队逻辑通过优先级队列排序 + 条件变量唤醒,实现了以下目标:

    • 保证元素按延迟时间从短到长排序,确保最优先到期的元素能被及时处理;

    • 当新的优先元素入队时,及时唤醒阻塞的取元素线程,避免线程不必要的等待;

    • 通过 leader 变量优化多线程竞争场景,减少无效的阻塞与唤醒开销。

6.3.3 出队take方法

public E take() throws InterruptedException {
    // 通过 ReentrantLock 加锁,保证出队操作的线程安全
    final ReentrantLock lock = this.lock;
    lock.lockInterruptibly();
    try {
        for (;;) {
            E first = q.peek(); // 调用 q.peek() 获取最早到期的元素,但不将其从队列中移除
            if (first == null) // 若队首元素为空(队列无元素)
                available.await(); // 当前线程通过 available.await() 进入无限期等待,同时释放锁
            else {
                long delay = first.getDelay(NANOSECONDS); // 堆顶元素的到期时间             
                if (delay <= 0) // 若队首元素已过期
                    return q.poll(); // 调用 q.poll() 弹出并返回该元素
                first = null;  // 若队首元素未过期(delay > 0),先将 first 置空方便 GC
                if (leader != null) // 若 leader 不为空(已有线程在处理)
                    available.await(); // 当前线程无限期等待
                else { // 若 leader 为空
                    // 将当前线程设为 leader
                    Thread thisThread = Thread.currentThread();
                    leader = thisThread;
                    try {
                        available.awaitNanos(delay); // 等待剩余延迟时间
                    } finally {
                        // 如果leader还是当前线程就把它置为空,让其它线程有机会获取元素
                        if (leader == thisThread)
                            leader = null;
                    }
                }
            }
        }
    } finally {
        if (leader == null && q.peek() != null) // 出队后,若 leader 为空且队列仍有元素
            available.signal(); // 唤醒阻塞在 available 上的下一个线程
        lock.unlock(); // 释放锁,完成出队操作
    }
}
  • DelayQueuetake 方法通过锁控制 + 优先级队列排序 + 条件变量等待/唤醒 + leader 线程优化,实现了以下目标:

    • 确保只有到期的元素才会被取出,严格遵循延迟执行逻辑;

    • 利用 leader 变量减少多线程竞争,避免多个线程同时等待同一个过期元素,提升效率;

    • 通过条件变量的精准唤醒,保证线程在元素到期时能及时被通知,避免无效等待。

6.4 如何选择适合的阻塞队列

6.4.1 选择策略

  • 功能维度:若需要阻塞队列具备排序能力(如优先级排序、延迟执行),则选择类似 PriorityBlockingQueue(优先级排序)、DelayQueue(延迟执行)的队列;

  • 容量维度:不同阻塞队列的容量特性差异很大,需根据任务数量推算合适容量,进而选择队列;

    • ArrayBlockingQueue:容量固定,初始化时必须指定;

    • LinkedBlockingQueue:默认无界(容量为 Integer.MAX_VALUE),也可指定有界;

    • SynchronousQueue:容量为0,无存储,直接传递元素;

    • DelayQueue:无界,容量固定为 Integer.MAX_VALUE

  • 能否扩容维度:若业务需动态扩容(如无法预估队列大小,应对高低峰),则不能选 ArrayBlockingQueue(容量创建时固定),可选择 PriorityBlockingQueue(支持自动扩容);

  • 内存结构维度:若对性能(空间效率)有要求,可从内存结构角度选择

    • ArrayBlockingQueue 基于数组实现,无链表节点开销,空间利用率高;

    • LinkedBlockingQueue 基于链表实现,有节点对象开销;

  • 性能维度

    • LinkedBlockingQueue锁分离(两把锁),高并发时性能优于单锁的 ArrayBlockingQueue

    • SynchronousQueue 因“直接传递”无存储过程,性能往往更优,适合需直接传递元素的场景。

6.4.2 线程池对于阻塞队列的选择

  • FixedThreadPool(包括 SingleThreadExecutor)选择 LinkedBlockingQueue:该队列可指定容量(或默认无界),适合线程数固定、任务需缓冲的场景,能平衡任务存储和并发处理效率;
  • CachedThreadPool 选择 SynchronousQueue:因为它无容量、直接传递元素,适合任务数量不确定但需快速处理的场景,可动态创建线程(空闲线程会回收),实现任务的高效“直接移交”;
  • ScheduledThreadPool(包括 SingleThreadScheduledExecutor)选择延迟队列:用于执行定时任务或延迟任务,能按时间优先级排序并执行任务,满足定时调度的需求。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

失散13

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值