很多面试官在问“synchronized不可中断”时,其实想考察的并不只是这一句话,而是你对JVM锁机制、线程状态、中断协作到底有没有完整理解。我第一次被问到这个问题的时候,张口就说“synchronized不能被中断”,结果面试官追问了一句“那它在什么情况下不能被中断?wait能不能被中断?”,我当场卡住了。后来啃了不少源码、做了不少实验,才把这条线彻底捋清楚。这篇文章就把我对“synchronized不可中断”含义的完整理解写出来,包含实验代码、排查思路和面试回答套路,希望能帮到你。
1. synchronized的“不可中断”到底指什么
1.1 先搞清楚“不可中断”说的是哪个阶段
很多人会把“不可中断”误解成“synchronized代码块一旦执行,就雷打不动、谁来了都拦不住”。这个说法不能算全错,但非常不精确。更准确地说,synchronized的不可中断指的是:一个线程在尝试获取synchronized锁时,如果锁已经被其他线程持有,那么这个线程会一直阻塞等待,直到持有锁的线程释放锁为止。在这个等待过程中,线程不会响应中断——即使你调用了Thread.interrupt(),它依然会傻傻地等在那里。
可以把这个过程类比成去银行柜台办业务。你取了一个号,坐在等候区等叫号,这个时候就算有人拍你肩膀说“别等了,你该走了”,你也不会真的离开,因为银行排队的规矩是“叫到号你才能上前”。这里的“叫号”就是锁被释放,而“有人拍你肩膀”就是别的线程给你发中断信号。在synchronized这种排队机制下,拍肩膀没用,你必须等到叫号。
从JVM内部看,这个“排队等待”发生在monitorenter指令上。JVM的monitor机制会记录当前哪个线程持有了这把锁,其余线程进入BLOCKED状态,在锁的等待队列里排队。JDK的线程状态枚举里有一个专门的BLOCKED状态,就是给“等锁”准备的。
code复制// 简化的线程状态枚举
public enum State {
NEW, // 新建
RUNNABLE, // 可运行
BLOCKED, // 阻塞等待monitor锁
WAITING, // 无限期等待
TIMED_WAITING,// 限期等待
TERMINATED // 终止
}
很多人在这个地方有个误区:把BLOCKED和WAITING混为一谈。其实在synchronized语境下,线程等锁时是BLOCKED状态,而sleep()、wait()等操作会让线程进入WAITING或TIMED_WAITING状态。这两种状态对中断的响应完全不同,稍后我会用代码演示。
1.2 面试里最常见的三种“答错姿势”
我听过不少候选人回答这个问题,归纳下来有三种典型误区,大家可以自己对号入座。
-
误区一:synchronized代码块里无法响应任何中断。 这是把“获取锁阶段不可中断”和“持有锁阶段不可中断”混为一谈了。实际上,一旦你进入了synchronized代码块,你还是可以调用
wait()、sleep()等可中断方法,这些方法在收到中断信号后会抛出InterruptedException。但要注意,synchronized本身持有锁的语义不会因为中断而放弃,所以你抛了异常,锁还是在你手上,必须等代码块退出才会释放。 -
误区二:不可中断就等于死锁。 这也不对。死锁是因为多个线程互相持锁、互相等待,导致谁都无法继续推进。不可中断只是说“等待锁的过程不受中断影响”,不代表一定会死锁。如果持有锁的那个线程很快执行完并释放锁,等待线程就能继续跑,根本没有死锁。死锁只是不可中断机制下最恶劣的一种可能性。
-
误区三:把“不可响应中断”说成“不可被停止”。 中断只是给线程设置一个标志位,并不是强制终止线程。一个被中断的线程,如果自己不检查中断位、不响应中断,它照样能继续运行。synchronized的不可中断,本质上是“不把中断标志当成排队离场的凭证”,而不是“系统不允许你停下来”。
把上面这些理解到位,“不可中断”的定义才算真正清晰。接下来我用代码把这个行为钉死,让大家能亲眼看到。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 用几行代码把“不可中断”钉死在案发现场
2.1 实验:让一个线程等锁,另一个线程去中断它
写一个最简单的实验:两个线程共享同一把锁,threadA先拿到锁并持有很久,threadB在锁外等着被threadA释放锁。这时我们让一个第三方线程threadC去调用threadB.interrupt(),然后观察threadB的行为。
java复制public class SynchronizedInterruptDemo {
private static final Object LOCK = new Object();
public static void main(String[] args) throws InterruptedException {
Thread threadA = new Thread(() -> {
synchronized (LOCK) {
System.out.println("threadA 拿到锁,开始长任务");
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
System.out.println("threadA 的 sleep 被中断");
}
System.out.println("threadA 释放锁");
}
}, "threadA");
Thread threadB = new Thread(() -> {
synchronized (LOCK) {
System.out.println("threadB 拿到锁,执行任务");
}
}, "threadB");
threadA.start();
Thread.sleep(200); // 确保threadA先拿到锁
threadB.start();
Thread.sleep(200); // 让threadB进入阻塞队列
System.out.println("threadB 状态: " + threadB.getState());
// threadC去中断正在等锁的threadB
Thread threadC = new Thread(() -> {
System.out.println("threadC 发起中断: threadB.interrupt()");
threadB.interrupt();
}, "threadC");
threadC.start();
Thread.sleep(500);
System.out.println("中断后 threadB 状态: " + threadB.getState());
System.out.println("中断后 threadB 中断标志: " + threadB.isInterrupted());
threadA.join();
threadB.join();
System.out.println("主线程执行结束");
}
}
运行结果类似下面这样:
code复制threadA 拿到锁,开始长任务
threadB 状态: BLOCKED
threadC 发起中断: threadB.interrupt()
中断后 threadB 状态: BLOCKED
中断后 threadB 中断标志: true
threadA 释放锁
threadB 拿到锁,执行任务
主线程执行结束
看到了吗?threadB在BLOCKED状态下被中断,它的中断标志位确实变成了true,但线程状态依然纹丝不动地停留在BLOCKED,短时间内线程没有被唤醒,而是等threadA释放锁之后才继续执行。这说明什么?synchronized在获取锁的等待阶段,根本不把中断当回事。 中断标志虽然被置上了,但JVM不会让BLOCKED状态的线程因为中断而放弃抢锁。
2.2 对比实验:ReentrantLock的lockInterruptibly为什么能中断
没有对比就没有伤害。我们把上面的代码改成ReentrantLock,并且使用lockInterruptibly()或lock(),看看效果有什么不同。
java复制import java.util.concurrent.locks.ReentrantLock;
public class LockInterruptDemo {
private static final ReentrantLock LOCK = new ReentrantLock();
public static void main(String[] args) throws InterruptedException {
Thread threadA = new Thread(() -> {
LOCK.lock();
try {
System.out.println("threadA 拿到锁,开始长任务");
Thread.sleep(5000);
} catch (InterruptedException e) {
System.out.println("threadA 的 sleep 被中断");
} finally {
System.out.println("threadA 释放锁");
LOCK.unlock();
}
}, "threadA");
Thread threadB = new Thread(() -> {
try {
LOCK.lockInterruptibly(); // 可中断获取锁
try {
System.out.println("threadB 拿到锁,执行任务");
} finally {
LOCK.unlock();
}
} catch (InterruptedException e) {
System.out.println("threadB 在等待锁时被中断,放弃获取锁");
}
}, "threadB");
threadA.start();
Thread.sleep(200);
threadB.start();
Thread.sleep(200);
System.out.println("threadB 初始状态: " + threadB.getState());
Thread threadC = new Thread(() -> {
System.out.println("threadC 发起中断: threadB.interrupt()");
threadB.interrupt();
}, "threadC");
threadC.start();
Thread.sleep(500);
System.out.println("中断后 threadB 状态: " + threadB.getState());
System.out.println("中断后 threadB 中断标志: " + threadB.isInterrupted());
threadA.join();
threadB.join();
System.out.println("主线程执行结束");
}
}
运行结果大概是:
code复制threadA 拿到锁,开始长任务
threadB 初始状态: WAITING
threadC 发起中断: threadB.interrupt()
中断后 threadB 状态: RUNNABLE
中断后 threadB 中断标志: false
threadB 在等待锁时被中断,放弃获取锁
threadA 释放锁
主线程执行结束
注意几个关键差异:
threadB用lockInterruptibly()等待锁时,线程状态是WAITING(AQS里通过LockSupport.park()挂起),而不是synchronized的BLOCKED。- 中断后,
lockInterruptibly()立即抛出InterruptedException,线程状态恢复成RUNNABLE,中断标志被清除。 threadB直接退出了抢锁流程,不再等待threadA释放锁。
这个对比非常直观地展示了“可中断”和“不可中断”在锁获取阶段的差别。底层原因也很好理解:ReentrantLock是纯Java实现(基于AQS),它提供了对中断响应的完整控制;而synchronized是JVM内置的monitor机制,属于重量级、偏底层的同步原语,目前JVM选择的设计就是“等待锁时不响应中断”。
2.3 锁获取阶段和锁持有阶段,中断表现完全不一样
拿上面两个实验做总结,可以把“synchronized不可中断”的边界用一张表画清楚:
| 阶段 | 是否响应中断 | 具体表现 |
|---|---|---|
| 锁获取阶段(没有抢到锁,线程BLOCKED等待) | 不响应 | 中断标志被置为true,但线程继续阻塞等待,直到拿到锁 |
| 锁持有阶段(已经进入synchronized代码块) | 看具体调用 | 如果代码里调用了wait()、sleep()等可中断方法,会抛InterruptedException;但锁的持有权不会因中断而释放 |
| 锁释放后 | 无影响 | 锁正常释放,等待队列中其他线程继续竞争锁 |
这个表格基本上把面试官想听到的边界都覆盖了。大家在回答时,最好是把这个“分阶段”的视角讲清楚,而不是笼统地说“synchronized不可中断”。
3. 不可中断不等于“进入synchronized后就不能响应中断”
3.1 synchronized块内调用wait()会发生什么
很多人对wait()有一个大误解,以为wait()只和synchronized相关,而且被中断了也没反应。实际情况恰恰相反:Object.wait()必须放在synchronized代码块里才合法,而且它本身是一个“可中断”的方法,线程进入WAITING状态后,一旦收到中断信号,就会抛出InterruptedException,同时它会先释放掉当前持有的锁。
看一个例子:
java复制public class WaitInterruptDemo {
private static final Object LOCK = new Object();
public static void main(String[] args) throws InterruptedException {
Thread threadA = new Thread(() -> {
synchronized (LOCK) {
System.out.println("threadA 持有锁,开始wait");
try {
LOCK.wait(10000);
System.out.println("threadA wait结束,继续执行");
} catch (InterruptedException e) {
System.out.println("threadA wait被中断,抛出InterruptedException");
}
System.out.println("threadA 仍然持有锁吗?继续执行到这里");
}
}, "threadA");
Thread threadB = new Thread(() -> {
synchronized (LOCK) {
System.out.println("threadB 拿到了锁");
}
}, "threadB");
threadA.start();
Thread.sleep(300);
threadB.start();
Thread.sleep(200);
new Thread(() -> {
System.out.println("中断threadA");
threadA.interrupt();
}).start();
threadA.join();
threadB.join();
}
}
运行结果中你会发现:threadA调用wait(10000)之后进入WAITING状态,threadB能立刻拿到锁(说明wait()确实释放了锁),之后threadA被中断,抛出InterruptedException,线程继续执行并最终退出锁代码块。
所以,synchronized块内“能不能响应中断”,取决于你调用的方法本身是不是可中断的。wait()就是典型的中断响应点。
3.2 synchronized块内调用sleep()时中断又是什么表现
sleep()是可中断方法,这点大家应该都知道。但如果一个线程已经持有synchronized锁,在代码块里调用sleep(),然后另一个线程中断它,会发生什么?
Thread.sleep()会响应中断并抛出InterruptedException,但它不会释放synchronized锁。这是因为sleep()在语义上并不会释放任何锁——它只是让当前线程暂时休眠。锁的释放必须等到synchronized代码块执行完毕。
code复制threadA 在 synchronized 块里 sleep(5000)
threadC 调用 threadA.interrupt()
threadA 抛出 InterruptedException
threadA 继续执行 synchronized 块内部的剩余代码(如果没退出的话)
这个细节在面试里经常被拿来出连环题:“既然你说不可中断,那我在synchronized里调用sleep被中断了怎么办?”你要能脱口而出:sleep会抛异常,但锁不会因为你被中断就释放,锁的释放点仍然在代码块出口。
3.3 连环追问:为什么wait必须放在synchronized里
这个问题是绕不开的,因为只要聊到synchronized和中断,面试官下一步大概率会问“那wait()为什么要放在synchronized里”。标准答案其实就一句话:wait()操作的是monitor的等待集,而monitor的访问权限是通过synchronized获得的。 如果不持有monitor就调用wait(),JVM会直接抛IllegalMonitorStateException。这是语法层面的强制约束。
更深入一点,从多线程安全角度理解:wait()要发挥作用,必须配合一个判断条件,比如队列空就等待、队列不空才消费。如果判断条件和wait()不在同一个锁保护下,就会出现经典问题——两个线程同时看到一个不满足的条件,然后都wait(),等notify()来了却都醒不来,产生“信号丢失”或“过早唤醒”。synchronized保证了判断、修改状态、调用wait()这三步是原子性的,这也是monitor设计的基本逻辑。
4. 实战避坑:死锁场景下为什么不能靠中断“救场”
4.1 synchronized死锁的典型场景复现
所谓死锁,就是两个或多个线程互相持有对方需要的锁,谁都不让。前面讲了synchronized等待锁阶段不可中断,这直接导致了一个结果:当synchronized造成死锁时,你没法通过interrupt()去唤醒任何一个线程,让它们放弃锁。 这在定位线上问题时相当痛苦。
我们写一个经典死锁:
java复制public class DeadLockDemo {
private static final Object LOCK_A = new Object();
private static final Object LOCK_B = new Object();
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(() -> {
synchronized (LOCK_A) {
System.out.println("thread1 拿到 LOCK_A,准备拿 LOCK_B");
try { Thread.sleep(100); } catch (InterruptedException e) {}
synchronized (LOCK_B) {
System.out.println("thread1 拿到 LOCK_B");
}
}
}, "thread1");
Thread thread2 = new Thread(() -> {
synchronized (LOCK_B) {
System.out.println("thread2 拿到 LOCK_B,准备拿 LOCK_A");
try { Thread.sleep(100); } catch (InterruptedException e) {}
synchronized (LOCK_A) {
System.out.println("thread2 拿到 LOCK_A");
}
}
}, "thread2");
thread1.start();
thread2.start();
Thread.sleep(500);
System.out.println("thread1 state = " + thread1.getState());
System.out.println("thread2 state = " + thread2.getState());
// 尝试用中断“救火”
thread1.interrupt();
thread2.interrupt();
Thread.sleep(300);
System.out.println("中断后 thread1 state = " + thread1.getState());
System.out.println("中断后 thread2 state = " + thread2.getState());
// 程序不会结束,因为两个线程都永远阻塞在BLOCKED
System.out.println("主线程结束,但业务线程依然互相等待");
System.exit(0);
}
}
输出大致是:
code复制thread1 拿到 LOCK_A,准备拿 LOCK_B
thread2 拿到 LOCK_B,准备拿 LOCK_A
thread1 state = BLOCKED
thread2 state = BLOCKED
中断后 thread1 state = BLOCKED
中断后 thread2 state = BLOCKED
主线程结束,但业务线程依然互相等待
两个线程都处于BLOCKED状态,即使调用了interrupt(),状态纹丝不动。这就是synchronized不可中断在死锁场景下的“威力”。
4.2 定位死锁只能用jstack,别指望中断
上面这种现场如果出现在生产环境,绝对不能靠interrupt()碰运气。用JDK自带的jstack命令可以快速找到死锁:
code复制jstack <pid>
在输出里,你会看到类似这样的内容:
code复制Found one Java-level deadlock:
=============================
"thread1":
waiting to lock monitor 0x000000001a00a0b8 (object 0x0000000712123456, a java.lang.Object),
which is held by "thread2"
"thread2":
waiting to lock monitor 0x000000001a00a0b8 (object 0x0000000712123456, a java.lang.Object),
which is held by "thread1"
Java stack information for the threads listed above:
...
jstack会自动检测死锁,并输出线程之间的锁依赖关系。这个信息是排查死锁的第一手资料,比你自己去猜要有用得多。找到死锁现场之后,一般只能两种解法:一是重启进程,让业务恢复,然后从代码层面修;二是在代码层面做预防,比如所有线程都按固定的全局顺序获取多个锁,或者用ReentrantLock配合tryLock()和超时机制,把死锁“可中断化”。
4.3 Lock与synchronized选型建议,别再无脑选一个
很多人一听到synchronized有“不可中断”这个缺陷,就想着全项目换成ReentrantLock。我的建议是:分场景,别走极端。
| 对比维度 | synchronized | ReentrantLock |
|---|---|---|
| 锁获取中断响应 | 不支持 | 支持 lockInterruptibly() |
| 尝试非阻塞获取锁 | 不支持 | 支持 tryLock() |
| 获取锁超时 | 不支持 | 支持 tryLock(long, TimeUnit) |
| 公平锁 | 不支持(基本上是抢占式) | 支持构造参数 new ReentrantLock(true) |
| 实现机制 | JVM内置monitor | AQS(Java代码) |
| 锁释放 | 自动释放(代码块结束) | 必须在finally中手动释放 |
我的个人经验是:如果一个锁的临界区很短,比如只是保护一个变量、一段很短的逻辑,优先用synchronized。因为JVM对synchronized做了很多锁优化(偏向锁、轻量级锁、锁消除等),代码简洁,不会出现忘记释放锁的问题。如果临界区操作耗时较长,或者你可能需要“等不到锁就放弃”、需要公平锁、需要支持多条件队列等高级用法,再考虑ReentrantLock。
与此同时,synchronized的“不可中断”也不是一无是处。有些场景里,锁就是要拿到底,比如全局状态切换、初始化流程,你是真的不希望线程因为被打断而丢下锁跑路,那样反而容易出问题。JVM选择让synchronized在锁等待阶段不响应中断,与其说是个缺陷,不如说是一种设计取舍。
5. 面试现场:这个问题怎么答才稳、怎么追问都不慌
5.1 一个可以“通用”的回答思路
如果面试官问“synchronized不可中断的含义”,我建议你按照“结论—原理—例子—对比”四步来答,这样逻辑连贯,也能展示深度。
- 结论先行:synchronized的不可中断,指的是线程在通过
monitorenter进入monitor时,如果锁被其他线程持有,当前线程会进入BLOCKED状态,一直等锁释放;在这个等待过程中,Thread.interrupt()不会让线程退出等待,只会把中断标志位置为true。 - 原理解释:synchronized是JVM内置的monitor机制,它的阻塞和唤醒走的是JVM内部的线程调度,并不像AQS那样有显式的中断响应逻辑。Java层面没有提供“获取synchronized锁时可以响应中断”的API,所以等待锁的线程对中断无感。
- 举例说明:可以用一个两个线程抢同一把锁的例子,说明一个线程在
BLOCKED时被interrupt(),状态依然是BLOCKED,直到锁释放才继续跑。 - 对比收尾:和
ReentrantLock的lockInterruptibly()做对比——后者在等待锁时收到中断,会立刻抛InterruptedException并放弃抢锁。这样就能说明,synchronized的不可中断更多是针对“锁获取阶段”,而一旦进入代码块内部,调用wait()、sleep()这些可中断方法时,线程依然可以响应中断。
按照这个顺序回答,基本能把面试官想要的关键点都覆盖到,而且不会漏掉最容易混淆的“获取阶段”和“持有阶段”。
5.2 常见的连环追问准备
回答完上面的框架,面试官通常会从下面几个方向继续挖,提前做好准备:
- 那线程的BLOCKED和WAITING状态有什么区别?
BLOCKED是在等monitor锁,不响应中断;WAITING是主动调用wait()、join()、park()等进入的等待状态,Object.wait()和Thread.sleep()等方法响应中断。这两者底层等待机制完全不同,不要再把synchronized等锁说成“WAITING”,它一定是BLOCKED。 - 为什么
synchronized不用在finally里释放锁? 因为JVM在字节码层面生成了monitorenter和monitorexit指令,通过异常表保证异常路径也会执行monitorexit,所以代码块结束自动释放。而ReentrantLock是纯Java实现,没有这种编译器层面的约束,必须手动unlock()。 - 现代JVM的synchronized性能还差吗? 在JDK 6之后,synchronized引入了偏向锁、轻量级锁、重量级锁的升级路径。绝大多数场景下,synchronized的锁开销已经大幅降低。只有在高竞争、长临界区的场景下,
ReentrantLock的效果才可能更明显。不要背老黄历说“synchronized性能很差”。 - 中断标志位需要手动清除吗? 通常情况下,被中断线程如果不重启,不需要特别关心。但如果你要在同一个线程里复用它,想再处理中断逻辑,就是另一个话题了。至少你要知道,
isInterrupted()返回的是当前中断标志,而Thread.interrupted()会清除标志位,这也是很多人在并发代码里踩过的坑。
5.3 从面试官视角说几句大实话
作为长期面试别人的技术人,我真心觉得“synchronized不可中断”不是一个孤立的知识点。你能把这个题答好,至少证明三件事:
- 你愿意深入JVM去看锁机制,而不是只背结论;
- 你能区分线程状态、锁获取、中断响应这些概念之间的关系;
- 你在真实项目中见过死锁、处理过并发问题,知道什么时候该选
Lock、什么时候该选synchronized。
这些能力,比单纯知道“不可中断”四个字值钱得多。所以,如果你想在面试中靠这道题加分,最好的方式不是背更多八股,而是自己在本地把这篇文章里的代码多跑几遍,把每个输出结果对应到概念上。自己试过一遍之后,再被追问任何细节,你都会更有底气。
最后再说一个我在实际项目里踩过的坑:有一次排查线上接口偶发超时,jstack发现大量线程阻塞在同一个synchronized方法上,一看代码,好家伙,这个方法里面调了一个外部HTTP接口,整个服务几十个线程全堵在锁上。当时我第一反应是“用ReentrantLock换掉它”,但后来冷静下来发现,根因其实是“持锁期间不应做耗时IO”。改回只锁内存数据、把IO调用挪到锁外之后,问题直接消失。这件事给我的教训是:synchronized不可中断本身不是罪,怎么设计临界区、怎么控制持锁时间,才真正考验功力。
