文章
阻塞队列被阻塞时,线程在干嘛?
线程在 BlockingQueue 阻塞时是“睡着”的,不占CPU,CPU 会被调度去执行其他线程
目录
当一个线程在调用 BlockingQueue 的阻塞方法(如 take()、put() 或带超时的 offer/poll)时,若队列当前状态不满足其操作条件(队列空或队列满),它并不是“空转”或“自旋”消耗 CPU,而是进入了 阻塞/等待 状态,具体过程大致如下:#
1. 高层逻辑:ReentrantLock + Condition#
以最常见的 ArrayBlockingQueue 为例,其内部通过一个 ReentrantLock 和两个 Condition(notEmpty、notFull)来实现阻塞与唤醒:
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
// 如果队列为空,就在 notEmpty 上等待
while (count == 0)
notEmpty.await(); //←—— 线程在这里被挂起
E x = dequeue();
// 唤醒可能在 notFull 上等待的生产者
notFull.signal();
return x;
} finally {
lock.unlock();
}
}
- **调用 **
notEmpty.await()- 线程会先 释放 已获取的锁(
lock.unlock()), - 然后进入 WAITING(无限等待)或 TIMED_WAITING(带超时等待)状态,
- JVM 底层通过
AbstractQueuedSynchronizer(AQS)将该线程 park(即挂起)到notEmpty条件队列中,不占用 CPU。
- 线程会先 释放 已获取的锁(
2. 线程状态#
- 在阻塞期间(
await()之后、被唤醒之前),该线程的Thread.getState()会是 WAITING 或 TIMED_WAITING。 - 它不再参与 CPU 调度,直到:
- 被其它线程
signal()/signalAll()唤醒(例如生产者执行了notEmpty.signal()), - 或者在带超时的方法中达到超时,自动唤醒。
- 被其它线程
3. 底层实现机制#
- LockSupport.park() / unpark()
- AQS 在实现
Condition.await()时,会调用LockSupport.park(this)挂起线程; Condition.signal()则会调用LockSupport.unpark(thread),将目标线程从停顿状态唤醒。
- AQS 在实现
- **线程重入 **
lock.lockInterruptibly()- 在被唤醒后,线程会首先尝试 重新获取
ReentrantLock才能继续执行后续代码。 - 如果在等待期间被中断,它会从
await()抛出InterruptedException并重新获取锁,然后退出。
- 在被唤醒后,线程会首先尝试 重新获取
4. 在操作系统层面#
- 虽然 Java 在用户态通过
LockSupport.park()管理线程挂起,但最终仍依赖 OS 的 线程调度器:- 被
park()的线程会让出 CPU,无上下文切换到内核态进行真正的阻塞, - 直到被
unpark()(或者中断、超时)后,OS 再将其唤醒并放回可运行队列。
- 被
5. 小结#
- 不占 CPU:线程在
BlockingQueue阻塞时是“睡着”的,CPU 会调度去执行其他线程。 - Condition 等待队列:Java 用
ReentrantLock+Condition(基于 AQS)实现挂起与唤醒。 - LockSupport.park/unpark:底层调用它进行线程挂起与恢复。
- 重获锁:被唤醒后,线程先拿回锁再继续操作。 了解这些机制,能帮助你:
- 优化并发性能(减少不必要的阻塞),
- 在 debug/诊断中正确识别线程状态(如在
jstack中看到 WAITING), - 并在必要时使用超时或非阻塞方法(
offer/poll)来设计更健壮的系统。