面试官:说说线程的阻塞、等待和自旋的区别?我:就这?

编程达人挑战赛·第6期 10w+人浏览 99人参与

🎯 阅读本文你将收获:彻底理解 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.WAITINGThread.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 消耗✅ 不消耗✅ 不消耗持续消耗
上下文切换❌ 有开销❌ 有开销无开销
响应速度中等中等极快
适用场景锁持有时间长线程间协作锁持有时间极短
线程状态BLOCKEDWAITINGRUNNABLE
代表 APIsynchronizedwait()/park()CAS/自旋锁

---

🎯 四、阻塞 vs 等待 —— 最易混淆的点

很多人分不清阻塞和等待,记住这个口诀:

阻塞是"被迫单身",等待是"主动单身"

对比项阻塞等待
原因抢锁失败,被迫等待主动调用 wait(),自愿等待
锁状态从未获得锁先获得锁,再释放
唤醒方式锁释放后自动竞争必须被 notify() 唤醒

代码对比:

// 阻塞:根本没进去过
synchronized (lock) {  // 👈 卡在门口进不去
    // ...
}

// 等待:进去了又主动出来
synchronized (lock) {  // 先进去
    lock.wait();       // 👈 主动出来等通知
}

🚀 五、实际应用场景

场景一:选择阻塞

  • synchronized 重量级锁
  • 锁持有时间较长
  • 线程数量多

场景二:选择等待

  • 生产者-消费者模式
  • 需要线程间通信
  • 条件不满足时主动让出

场景三:选择自旋

  • 锁持有时间极短(纳秒级)
  • CPU 核心数足够
  • 竞争不激烈

💡 六、JVM 的智慧——自适应自旋

JVM 很聪明,它会根据情况 自动选择策略

  1. 第一次:自旋等待(赌锁很快释放)
  2. 自旋失败:增加自旋次数再试
  3. 多次失败:放弃自旋,升级为阻塞(别浪费 CPU 了)

这就是 自适应自旋锁 的原理!


🎵 七、记忆口诀

口诀一:三种状态

阻塞被动等锁开,等待主动盼人来,自旋原地转圈圈。

口诀二:CPU消耗

阻塞等待都睡觉,自旋转圈累到爆。

口诀三:选择策略

锁长用阻塞,协作用等待,锁短用自旋。

---

📝 八、面试题速答

Q1:阻塞和等待的区别?

阻塞是被动的(抢锁失败),等待是主动的(调用wait)。

Q2:自旋的优缺点?

优点:无上下文切换,响应快。缺点:消耗 CPU,不适合长时间等待。

Q3:什么时候用自旋?

锁持有时间极短、竞争不激烈、CPU 核心充足。

Q4:BLOCKED 和 WAITING 都是等待,有啥不同?

BLOCKED 在等锁(入口处排队),WAITING 在等通知(休息室等电话)。


🎁 总结

---

Java线程"等待"的三种姿势:

🚫 阻塞 (BLOCKED)

  • 触发:synchronized 抢锁失败
  • CPU:不消耗 ✅
  • 特点:被动等待,自动唤醒

⏳ 等待 (WAITING)

  • 触发:主动调用 wait()/park()
  • CPU:不消耗 ✅
  • 特点:主动放弃,需要唤醒

🔄 自旋 (SPINNING)

  • 触发:CAS 循环/自旋锁
  • CPU:持续消耗 ❌
  • 特点:响应极快,适合短等待
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

Apple_Web

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

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

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

打赏作者

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

抵扣说明:

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

余额充值