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
返回false
,poll
返回null
)。适用于“可容忍等待超时”的场景; -
add
/remove
/element
:与普通Queue
行为一致,队列满/空时抛异常,一般用于队列容量确定、不允许满/空的场景; -
offer
/poll
/peek
:队列满/空时返回特定值(false
/null
),适用于无需阻塞、温和处理满/空的场景。
-
1.3 应用场景
-
线程池
-
线程池的任务队列是阻塞队列。当提交的任务数超过线程池的处理能力时,新任务会进入阻塞队列等待;若队列为空,工作线程会被阻塞,直到有新任务提交;
-
通过阻塞式入队/出队实现任务的有序调度和线程同步,避免了手动处理线程等待、唤醒的复杂度,保证线程池的稳定运行;
-
-
生产者-消费者模型
-
生产者线程向阻塞队列中生产元素,消费者线程从队列中消费元素。若队列满,生产者阻塞;若队列空,消费者阻塞;
-
完美解决了生产者和消费者之间的并发协作问题,无需手动加锁即可实现“生产-消费”的有序流程,避免线程竞争和数据冲突;
-
-
消息队列
-
生产者将消息放入阻塞队列,消费者从队列中取出消息处理;
-
实现异步通信和解耦——生产者无需等待消费者处理即可继续生产,不同组件通过队列间接交互;同时利用阻塞特性保证消息的有序处理,提升系统吞吐量和响应性能;
-
-
缓存系统
-
缓存数据更新时,将新数据放入阻塞队列;其他线程从队列中获取最新数据使用;
-
避免了并发更新缓存时的竞争冲突,保证多线程对缓存数据的“读取-更新”操作有序且安全;
-
-
并发任务处理
-
将待处理任务放入阻塞队列,多个工作线程从队列中取任务并行处理;
-
避免多个线程重复处理同一任务,同时实现“任务提交”与“任务执行”的解耦,提升系统的可维护性和可扩展性。
-
1.4 JUC包下的阻塞队列
BlockingQueue
接口的实现类都被放在了 JUC 包中,它们的区别主要体现在存储结构上或对元素操作上的不同,但是对于take
与put
操作的原理却是类似的;
阻塞队列实现类 | 存储结构 | 核心特性 | 适用场景 |
---|---|---|---|
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)是并发编程中的一个真实存在的现象,它指的是一个等待线程在没有被其他线程通过
notify
、notifyAll
或条件满足的情况下被唤醒;以
ArrayBlockingQueue
的put
方法为例:- 当队列满时,生产者线程确实会在
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
对比维度 | ArrayBlockingQueue | LinkedBlockingQueue |
---|---|---|
队列大小 | 有界,初始化必须指定容量 | 可有界(指定容量)或无界(默认 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
方法,offer
是DelayQueue
入队操作的核心实现: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(); // 释放锁,完成出队操作
}
}
-
DelayQueue
的take
方法通过锁控制 + 优先级队列排序 + 条件变量等待/唤醒 + 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
)选择延迟队列:用于执行定时任务或延迟任务,能按时间优先级排序并执行任务,满足定时调度的需求。