文章目录
🎯 阅读本文你将收获:彻底理解 Java 线程的三种"等待"姿势,面试再也不慌!
📖 前言
很多 Java 开发者都被这个问题困扰过:
线程的 阻塞(Blocked)、等待(Waiting) 和 自旋(Spinning) 到底有什么区别?
网上的解释要么太官方看不懂,要么就是一堆源码直接劝退。
今天,我用一个 “去火锅店吃饭” 的例子,让你一次性彻底搞懂!
🍲 一、先来个生活例子(秒懂版)
想象一个场景:你去一家 网红火锅店 吃饭,但是没有空位了…
这时候你有三种等待方式:

🚫 方式一:阻塞(Blocked)—— 傻站着排队
- 你在干嘛:站在门口排队,哪也不去
- 状态:可以闭眼休息(不耗精力)
- 结束条件:等店员喊"XX号请进"
- 特点:被动等待,无法主动离开
就像线程抢 synchronized 锁失败,只能乖乖排队等着。
⏳ 方式二:等待(Waiting)—— 留号去逛街
- 你在干嘛:留个电话,去隔壁商场逛街
- 状态:可以做别的事(不耗精力)
- 结束条件:等店家打电话通知你
- 特点:主动放弃,需要别人唤醒
就像线程调用 wait() 方法,主动让出资源,等别人 notify()。
🔄 方式三:自旋(Spinning)—— 疯狂问"有位了吗"
- 你在干嘛:不停地问服务员"有位置了吗?有位置了吗?"
- 状态:很消耗精力(累死了!)
- 结束条件:一有空位立刻知道
- 特点:适合快要有空位时使用
就像线程用 CAS 不断循环检测,虽然累但响应超快!
💡 一张图总结
| 方式 | 你在干嘛 | 耗不耗精力 | 谁通知你 |
|---|---|---|---|
| 阻塞 | 😴 傻站排队 | 不耗(睡着了) | 店员自动叫号 |
| 等待 | 🛍️ 出去逛街 | 不耗(在玩) | 店家打电话 |
| 自旋 | 🏃 原地转圈问 | 超级耗! | 自己发现的 |
🔧 二、技术角度深入理解
好了,例子讲完了,我们来看看真正的技术细节。
2.1 阻塞(BLOCKED)
什么时候发生?
线程尝试获取 synchronized 锁,但锁被别人占着。
状态特点:
- 线程被 操作系统挂起,不占用 CPU
- 涉及 用户态到内核态的切换(开销大)
- 线程状态:
Thread.State.BLOCKED

代码示例:
public class BlockedDemo {
private static final Object lock = new Object();
public static void main(String[] args) {
// 线程1:先抢到锁,持有10秒
new Thread(() -> {
synchronized (lock) {
System.out.println("线程1:锁是我的!");
try { Thread.sleep(10000); } catch (Exception e) {}
}
}, "线程1").start();
// 线程2:抢锁失败,进入 BLOCKED 状态
new Thread(() -> {
synchronized (lock) { // 👈 在这里阻塞!
System.out.println("线程2:终于轮到我了!");
}
}, "线程2").start();
}
}
2.2 等待(WAITING / TIMED_WAITING)
什么时候发生?
线程 主动调用 wait()、join()、LockSupport.park() 等方法。
状态特点:
- 线程 主动释放 CPU 和锁资源
- 需要其他线程 显式唤醒
- 线程状态:
Thread.State.WAITING或Thread.State.TIMED_WAITING

代码示例:
public class WaitingDemo {
private static final Object lock = new Object();
public static void main(String[] args) throws Exception {
Thread t1 = new Thread(() -> {
synchronized (lock) {
System.out.println("线程1:我要等待了...");
try {
lock.wait(); // 👈 主动进入等待!
} catch (Exception e) {}
System.out.println("线程1:我被唤醒了!");
}
});
t1.start();
Thread.sleep(1000);
// 唤醒等待的线程
synchronized (lock) {
lock.notify(); // 👈 唤醒!
System.out.println("主线程:已发送唤醒通知");
}
}
}
2.3 自旋(Spinning)
什么时候发生?
使用 CAS 操作 或 自旋锁 循环检测条件。
状态特点:
- 线程 不让出 CPU,一直在循环检查
- 没有上下文切换,但 消耗 CPU 资源
- 线程状态:仍然是
Thread.State.RUNNABLE(因为还在跑!)

代码示例:
public class SpinningDemo {
private static AtomicBoolean lock = new AtomicBoolean(false);
public static void main(String[] args) {
// 自旋锁实现
new Thread(() -> {
System.out.println("线程1:开始自旋等待...");
// 👇 自旋!不断循环检测
while (!lock.compareAndSet(false, true)) {
// 啥也不干,就是转圈圈
}
System.out.println("线程1:获取到锁了!");
}).start();
}
}
📊 三、三者对比(面试必背)
| 对比维度 | 阻塞 (Blocked) | 等待 (Waiting) | 自旋 (Spinning) |
|---|---|---|---|
| 触发方式 | 被动(锁竞争失败) | 主动(调用wait等) | 主动(循环检测) |
| CPU 消耗 | ✅ 不消耗 | ✅ 不消耗 | ❌ 持续消耗 |
| 上下文切换 | ❌ 有开销 | ❌ 有开销 | ✅ 无开销 |
| 响应速度 | 中等 | 中等 | ⚡ 极快 |
| 适用场景 | 锁持有时间长 | 线程间协作 | 锁持有时间极短 |
| 线程状态 | BLOCKED | WAITING | RUNNABLE |
| 代表 API | synchronized | wait()/park() | CAS/自旋锁 |

🎯 四、阻塞 vs 等待 —— 最易混淆的点
很多人分不清阻塞和等待,记住这个口诀:
阻塞是"被迫单身",等待是"主动单身"
| 对比项 | 阻塞 | 等待 |
|---|---|---|
| 原因 | 抢锁失败,被迫等待 | 主动调用 wait(),自愿等待 |
| 锁状态 | 从未获得锁 | 先获得锁,再释放 |
| 唤醒方式 | 锁释放后自动竞争 | 必须被 notify() 唤醒 |
代码对比:
// 阻塞:根本没进去过
synchronized (lock) { // 👈 卡在门口进不去
// ...
}
// 等待:进去了又主动出来
synchronized (lock) { // 先进去
lock.wait(); // 👈 主动出来等通知
}
🚀 五、实际应用场景
场景一:选择阻塞
- synchronized 重量级锁
- 锁持有时间较长
- 线程数量多
场景二:选择等待
- 生产者-消费者模式
- 需要线程间通信
- 条件不满足时主动让出
场景三:选择自旋
- 锁持有时间极短(纳秒级)
- CPU 核心数足够
- 竞争不激烈
💡 六、JVM 的智慧——自适应自旋
JVM 很聪明,它会根据情况 自动选择策略:
- 第一次:自旋等待(赌锁很快释放)
- 自旋失败:增加自旋次数再试
- 多次失败:放弃自旋,升级为阻塞(别浪费 CPU 了)
这就是 自适应自旋锁 的原理!
🎵 七、记忆口诀
口诀一:三种状态
阻塞被动等锁开,等待主动盼人来,自旋原地转圈圈。
口诀二:CPU消耗
阻塞等待都睡觉,自旋转圈累到爆。
口诀三:选择策略
锁长用阻塞,协作用等待,锁短用自旋。

📝 八、面试题速答
Q1:阻塞和等待的区别?
阻塞是被动的(抢锁失败),等待是主动的(调用wait)。
Q2:自旋的优缺点?
优点:无上下文切换,响应快。缺点:消耗 CPU,不适合长时间等待。
Q3:什么时候用自旋?
锁持有时间极短、竞争不激烈、CPU 核心充足。
Q4:BLOCKED 和 WAITING 都是等待,有啥不同?
BLOCKED 在等锁(入口处排队),WAITING 在等通知(休息室等电话)。
🎁 总结

Java线程"等待"的三种姿势:
🚫 阻塞 (BLOCKED)
- 触发:synchronized 抢锁失败
- CPU:不消耗 ✅
- 特点:被动等待,自动唤醒
⏳ 等待 (WAITING)
- 触发:主动调用 wait()/park()
- CPU:不消耗 ✅
- 特点:主动放弃,需要唤醒
🔄 自旋 (SPINNING)
- 触发:CAS 循环/自旋锁
- CPU:持续消耗 ❌
- 特点:响应极快,适合短等待


被折叠的 条评论
为什么被折叠?



