【多线程】阻塞等待(Blocking Wait)(以Java为例)
【多线程】阻塞等待(Blocking Wait)(以Java为例)
本文来自于我关于多线程的系列文章。欢迎阅读、点评与交流
1.【多线程】互斥锁(Mutex)是什么?
2.【多线程】临界区(Critical Section)是什么?
3.【多线程】计算机领域中的各种锁
4.【多线程】信号量(Semaphore)是什么?
5.【多线程】信号量(Semaphore)常见的应用场景
6.【多线程】条件变量(Condition Variable)是什么?
7.【多线程】监视器(Monitor)是什么?
8.【多线程】什么是原子操作(Atomic Operation)?
9.【多线程】竞态条件(race condition)是什么?
10.【多线程】无锁数据结构(Lock-Free Data Structures)是什么?
11.【多线程】线程休眠(Thread Sleep)的底层实现
12.【多线程】多线程的底层实现
13.【多线程】读写锁(Read-Write Lock)是什么?
14.【多线程】死锁(deadlock)
15.【多线程】线程池(Thread Pool)
16.【多线程】忙等待/自旋(Busy Waiting/Spinning)
17.【多线程】阻塞等待(Blocking Wait)(以Java为例)
18.【多线程】阻塞等待(Blocking Wait)(以C++为例)
19.【多线程】屏障(Barrier)
20.【多线程硬件机制】总线锁(Bus Lock)是什么?
21.【多线程硬件机制】缓存锁(Cache Lock)是什么?
阻塞等待(Blocking Wait) 是一个并发编程中的核心概念之一。
1. 什么是阻塞等待?
阻塞等待指的是一个线程在执行过程中,由于某些条件暂时不满足(例如等待I/O操作完成、等待获取锁、等待另一个线程的结果等),而主动或被动地暂停自己的执行,让出CPU资源,进入一种“休眠”状态。直到它所等待的条件被满足后,才会被唤醒,重新进入就绪状态,等待CPU调度继续执行。
简单来说就是:线程停下来,等某个事情发生。
2. 为什么需要阻塞等待?
如果没有阻塞等待机制,线程在条件不满足时只能不停地循环检查(即“忙等待”或“自旋”),这会白白浪费宝贵的CPU时间片。
对比一下:
-
阻塞等待:
- 线程:”锁还没释放?那我先睡了,锁释放了记得叫醒我。“
- 优点: 不占用CPU,节能高效。
- 缺点: 线程切换会带来一定的上下文切换开销。
-
忙等待:
- 线程:”锁还没释放?我查一下…还没…我再查一下…还没…“
- 优点: 响应及时,一旦条件满足可立即继续。
- 缺点: 持续占用CPU,浪费资源,可能导致性能问题。
在绝大多数应用场景下,阻塞等待是更优的选择,因为CPU时间是宝贵的,应该留给真正需要计算的线程。
3. 常见的阻塞等待场景(附代码示例)
以下是一些在Java中典型的会导致线程阻塞等待的情况。
场景一:同步锁(synchronized 或 Lock)
当多个线程竞争同一个同步锁时,只有一个线程能成功获取,其他线程会被阻塞,直到锁被释放。
public class SynchronizedBlockingExample {private static final Object lock = new Object();public static void main(String[] args) {Thread t1 = new Thread(() -> {synchronized (lock) {System.out.println("线程1获取到锁,开始执行");try {Thread.sleep(3000); // 模拟耗时操作,持有锁} catch (InterruptedException e) {e.printStackTrace();}System.out.println("线程1释放锁");}});Thread t2 = new Thread(() -> {System.out.println("线程2尝试获取锁...");synchronized (lock) { // 线程2会在这里阻塞等待,直到线程1释放锁System.out.println("线程2获取到锁,开始执行");}});t1.start();t2.start();}
}
输出:
线程1获取到锁,开始执行
线程2尝试获取锁...
(等待约3秒后)
线程1释放锁
线程2获取到锁,开始执行
场景二:等待通知机制(wait/notify)
线程可以在持有锁的情况下,主动调用 wait()
方法释放锁并进入等待状态,直到其他线程调用 notify()
或 notifyAll()
将其唤醒。
public class WaitNotifyExample {private static final Object lock = new Object();private static boolean condition = false;public static void main(String[] args) throws InterruptedException {Thread waitingThread = new Thread(() -> {synchronized (lock) {System.out.println("等待线程:检查条件,条件不满足,开始等待");while (!condition) { // 必须用循环检查,防止虚假唤醒try {lock.wait(); // 释放锁,并进入WAITING状态(阻塞)} catch (InterruptedException e) {e.printStackTrace();}}System.out.println("等待线程:条件满足,继续执行");}});Thread notifyingThread = new Thread(() -> {synchronized (lock) {System.out.println("通知线程:正在改变条件...");condition = true;lock.notifyAll(); // 唤醒所有在lock上等待的线程System.out.println("通知线程:已发出通知");}});waitingThread.start();Thread.sleep(1000); // 确保等待线程先启动并进入等待notifyingThread.start();}
}
场景三:线程合并(Thread.join)
一个线程等待另一个线程执行完毕。
public class JoinExample {public static void main(String[] args) throws InterruptedException {Thread workerThread = new Thread(() -> {try {System.out.println("工作线程开始工作...");Thread.sleep(2000);System.out.println("工作线程结束工作。");} catch (InterruptedException e) {e.printStackTrace();}});workerThread.start();System.out.println("主线程等待工作线程结束...");workerThread.join(); // 主线程在这里阻塞,等待workerThread执行完毕System.out.println("主线程继续执行。");}
}
场景四:阻塞队列(BlockingQueue)
当队列为空时,从队列中获取元素的操作会被阻塞;当队列已满时,向队列中添加元素的操作也会被阻塞。
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;public class BlockingQueueExample {public static void main(String[] args) throws InterruptedException {BlockingQueue<String> queue = new ArrayBlockingQueue<>(3); // 容量为3// 生产者线程Thread producer = new Thread(() -> {try {queue.put("Item1");queue.put("Item2");queue.put("Item3");System.out.println("生产者已放满3个物品,尝试放第4个...");queue.put("Item4"); // 这里会被阻塞,直到有消费者消费一个物品System.out.println("生产者成功放入第4个物品");} catch (InterruptedException e) {e.printStackTrace();}});// 消费者线程Thread consumer = new Thread(() -> {try {Thread.sleep(3000); // 模拟消费者启动慢String item = queue.take(); // 消费一个物品,唤醒被阻塞的生产者System.out.println("消费者取走了: " + item);} catch (InterruptedException e) {e.printStackTrace();}});producer.start();consumer.start();}
}
4. 线程的状态与阻塞
在Java的 Thread.State
枚举中,与阻塞等待相关的状态主要有:
- BLOCKED: 线程等待获取一个同步锁(如进入
synchronized
块)而陷入的阻塞。这是被动阻塞。 - WAITING: 线程主动调用
Object.wait()
,Thread.join()
,LockSupport.park()
等方法后进入的状态。需要其他线程将其唤醒。 - TIMED_WAITING: 与
WAITING
类似,但设定了超时时间(如Thread.sleep(millis)
,Object.wait(timeout)
,Thread.join(timeout)
)。
总结
特性 | 阻塞等待 | 忙等待 |
---|---|---|
CPU占用 | 不占用 | 持续占用 |
实现机制 | 依靠操作系统/虚拟机线程调度 | 循环检查条件 |
响应速度 | 较慢(需要上下文切换) | 很快(条件满足立即执行) |
适用场景 | 绝大多数高并发、I/O密集型场景 | 低竞争、等待时间极短的场景(通常用自旋锁 ) |
理解阻塞等待是编写高效、正确并发程序的基础。它帮助你管理线程间的协作,避免资源竞争,并有效利用系统资源。