《写多线程总踩坑?死锁、可见性、wait/notify 核心问题全解析》
摘要:本文探讨了多线程编程中的死锁与线程通信问题。死锁的四个核心条件包括互斥、请求保持、不可剥夺和循环等待,可通过打破嵌套锁、统一加锁顺序等方法避免。线程间可见性问题可通过volatile关键字解决。wait/notify机制详解:wait()会释放锁并阻塞,notify()唤醒线程后需重新竞争锁,必须遵循先wait后notify的顺序。正确使用这些机制能确保线程安全通信,避免资源竞争问题。(14
一、 死锁的核心条件
- 互斥条件:资源只能被一个线程独占使用,其他线程需等待直到资源释放。
- 请求与保持条件:线程已持有部分资源,又提出新的资源请求,且不释放已持有的资源。
- 不可剥夺条件:线程已获得的资源不能被强制剥夺,只能由线程主动释放。
- 循环等待条件:多个线程形成环形链,每个线程都在等待链中下一个线程所持有的资源
死锁避免方法
-
打破 “嵌套锁” 改为 “并列锁”
- 原理:破坏 “请求与保持” 条件。原本线程在持有一把锁的同时请求另一把锁(嵌套),改为同时申请多把锁(并列),避免部分持有资源后再请求新资源的情况。
- 示例:若需同时操作
lock1和lock2,可设计一个方法同时获取这两把锁,而非先拿lock1再拿lock2。
-
对加锁顺序做出约定
- 原理:破坏 “循环等待” 条件。所有线程严格按照约定的同一顺序(如先
lock1后lock2)获取锁,避免形成环形等待链。 - 示例:线程 A 和线程 B 都需操作
lock1和lock2,则统一约定先获取lock1,再获取lock2,杜绝因加锁顺序混乱导致的循环等待。
- 原理:破坏 “循环等待” 条件。所有线程严格按照约定的同一顺序(如先
二、线程间的 “可见性” (详细讲解请看该文章标题四)

1. 代码逻辑与问题本质
- 线程 t1:循环判断
flag == 0,如果flag不变,就一直循环; - 线程 t2:通过控制台输入修改
flag的值; - 问题:即使 t2 修改了
flag为非 0,t1 的循环也不会停止 —— 因为 t1 “看不到”flag的变化,这是 Java 多线程的 “可见性” 缺陷 (默认情况下,一个线程修改共享变量,其他线程可能缓存旧值,感知不到新值)。
2. “卡点等待” 思路为什么不对?
“让 t1 先等 5 秒再执行循环” 只是改变了执行时机,但没解决 “可见性” 问题:
- 即使 t1 等 5 秒后再循环,只要 t2 在 5 秒内修改了
flag,t1 依然可能因为 “看不到新值” 而继续循环; - 反之,如果 t2 输入很慢(超过 5 秒),t1 的循环才会在 “看到新值” 后停止 —— 但这是靠 “时间巧合”,不是靠 “逻辑正确”,完全不可靠。
3. 正确的解决方法:保证 “可见性”
要让 t1 能感知到 t2 对flag的修改,需要给flag加上 volatile关键字 (它的核心作用就是保证 “可见性”,让一个线程的修改能被其他线程立即感知)。
三、wait和notify
1.wait()⽅法:
- 让当前执行代码的线程进入等待状态,被放入等待队列。
- 释放当前持有的锁。
- 在满足特定条件时被唤醒,重新尝试获取锁。
使用约束
必须搭配synchronized使用,若脱离synchronized使用wait,会直接抛出异常。
结束等待的条件
- 其他线程调用该对象的
notify方法。 wait方法的带超时参数版本(wait(timeout))达到等待时间。- 其他线程调用该等待线程的
interrupted方法,导致wait抛出InterruptedException异常。

步骤 1:进入synchronized块 —— 加锁
当线程执行到 synchronized (object) 时,会尝试获取 object 的对象锁,获取成功后进入同步块执行。
步骤 2:执行object.wait() —— 解锁并阻塞
线程执行 object.wait() 时,会做两件事:
- 释放当前持有的
object对象锁,让其他线程可以竞争这把锁; - 线程进入等待状态(阻塞在
wait()处),直到被notify()/notifyAll()唤醒。
步骤 3:被唤醒后 —— 重新加锁
当其他线程调用 object.notify() 或 object.notifyAll() 唤醒该线程时,它会:
- 重新尝试竞争
object的对象锁(与其他线程公平竞争); - 竞争成功后,从
wait()方法的位置继续向下执行。
步骤 4:退出synchronized块 —— 最终解锁
当线程执行到 synchronized 块的 } 时,会释放object的对象锁,完成整个同步逻辑。
简单总结流程:加锁 → wait(解锁+阻塞)→ 被唤醒(重新加锁)→ 执行后续逻辑 → 解锁(退出同步块)
总结:
- 这种设计确保了 “等待 - 唤醒” 机制的线程安全,既让等待的线程暂时释放锁给其他线程,又能在唤醒后重新获得锁以保证执行的原子性。
wait():线程 “暂时让渡锁 + 阻塞等待”,不是放弃工作,而是等条件满足。notify():告诉等待的线程 “条件满足了,快来复工”,避免线程一直阻塞。- 重新加锁:保证后续工作的原子性(同一时间只有一个线程操作共享资源)。
}解锁:线程完成所有工作后,彻底释放锁,让其他线程继续使用。
2.notify()方法:
核心功能
用于唤醒等待的线程。
调用约束
需在同步方法或同步块中调用。
唤醒机制
- 若有多个线程等待,由线程调度器随机挑选一个处于
wait状态的线程进行唤醒,无 “先来后到” 顺序。 - 调用
notify()后,当前线程不会立即释放对象锁,需等执行notify()的线程退出同步代码块后才会释放。
作用逻辑
通知等待该对象锁的其他线程,使它们重新尝试获取该对象的对象锁。
3.wait()和notify()方法调用时序:
关键规则
- 必须确保先执行
wait(),后执行notify(),这样notify()才能唤醒wait()的线程;若先notify()后wait(),wait()无法被唤醒。 notify()一个没有处于wait状态的对象,不会报错(即无副作用),只是 “一炮打空”,但执行notify()的线程自身不会抛出异常或出现其他报错。
4.代码演示“唤醒与等待”逻辑:

流程:wait()线程释放锁进等待队列 → 唤醒线程必须先拿同一把锁才能notify() → notify()只 “叫醒” 不 “让权” → 唤醒线程执行完同步代码块、释放锁后 → 被唤醒的线程才重新竞争锁 → 竞争到锁才能继续执行后续代码(竞争锁时可能短暂阻塞)。
关键细节:
-
唤醒线程的 “拿锁前提”:必须先占锁,才能唤醒没错!
notify()的本质是 “通知持有同一把锁的等待线程”,而要调用notify(),前提是当前线程已经通过synchronized (lock)拿到了这把锁 —— 如果没拿锁就调用lock.notify(),会直接抛出IllegalMonitorStateException异常(之前说的 “wait()/notify()必须搭配synchronized”,本质就是这个要求)。 -
被唤醒线程的 “阻塞场景”:只在 “竞争锁” 时可能阻塞,不是额外等待你说的 “唤醒之后还得阻塞等待一会”,准确来说是:
notify()后,被唤醒的线程会从 “等待队列” 转到 “锁的竞争队列”(和其他可能也在抢这把锁的线程一起排队);- 只有当唤醒线程退出同步代码块、释放锁后,竞争队列里的线程才会抢锁;
- 如果此时没有其他线程抢锁,被唤醒的线程会直接拿到锁,立刻继续执行
wait()后面的代码(几乎没有阻塞); - 如果有其他线程也在抢这把锁,就会短暂阻塞,直到抢到锁为止。
简单说:唤醒后不是 “固定阻塞一会”,而是 “按需竞争锁”—— 有竞争就等,没竞争就直接执行。
- 同步代码块 = 带锁的房间,
lock= 房间钥匙; - 等待线程:先拿到钥匙(进入同步代码块)→ 喊 “我先等会,钥匙先还回去”(
wait()释放锁,进等待队列)→ 站在门外等通知; - 唤醒线程:先拿到同一把钥匙(进入同步代码块)→ 喊 “里面那个等的可以进来了”(
notify())→ 继续在房间里做完自己的事(执行同步代码块剩余逻辑)→ 出门还钥匙(释放锁); - 等待线程:听到通知后,跑到门口和其他想进房间的线程一起抢钥匙(竞争锁)→ 抢到钥匙就进房间,继续做之前没做完的事(
wait()后面的代码)→ 做完出门还钥匙。
5.notifyAll()⽅法
notify⽅法只是唤醒某⼀个等待线程.使⽤notifyAll⽅法可以⼀次唤醒所有的等待线程.
6.补充问题疑惑区:lock.wait() 唤醒后流程:
lock.wait() 唤醒后流程:唤醒→自动抢锁→抢到后从 wait() 下一行执行→while 循环回头判断 state→符合则执行业务,不符合则再次 wait()
唤醒后抢锁成功,直接从 wait() 下一行执行,while 会强制回头再判断 state,if 不会,这就是两者的本质区别。
synchronized (lock) {
while (state != 目标值) { // 唤醒后必走这行(因为循环要回头判断)
lock.wait(); // 唤醒后从这行的下一行开始执行
// 👇 唤醒后先执行这里的代码,再回到上面的 while 条件判断
}
// 业务逻辑
}
synchronized (lock) {
if (state != 目标值) { // 唤醒后不走这行(因为 if 只判断一次)
lock.wait(); // 唤醒后从这行的下一行开始执行
// 👇 唤醒后直接执行这里的代码,不会回头判断 if
}
// 业务逻辑
}
关键细节(避免踩坑)
-
“执行 synchronized (lock)” 是抢锁结果,不是主动执行代码:你认为 “底层自动实现 执行 synchronized (lock) {”,更准确的是:唤醒后的线程会自动参与抢锁,抢到锁后,才能进入
synchronized同步块,继续执行wait()后面的代码 —— 不是 “主动执行 synchronized 这行代码”,而是抢锁成功后的 “进入权限”。 -
while 的 “必须执行” 是循环特性,不是额外逻辑:不是 “因为写了 while 就必须多执行一次”,而是
wait()唤醒后,代码从wait()下一行继续走,走到while循环的末尾,会自动回头判断循环条件(这是while本身的循环逻辑)—— 相当于 “自然回头查一下 state 对不对”。 -
if 不是 “不用执行”,是 “执行过一次就不回头”:if 也会执行一次判断(在
wait()之前),但唤醒后,代码不会回头再执行 if 判断 —— 相当于 “查过一次就不管了,直接往下走”。
场景 1:wait() 在 while 循环内
先再明确代码执行流程:
synchronized (lock) {
while (state != 目标值) { // ① 条件判断点
lock.wait(); // ② 等待点:释放锁+阻塞
// ③ 唤醒后执行点:wait() 的下一行
}
// ④ 业务逻辑
}
唤醒后的完整流程:
- 线程被唤醒 → 自动抢锁 → 抢到锁后,从 ③ 开始执行(
wait()的下一行); - 执行完 ③ 后,因为是
while循环,会自动回头执行 ①(条件判断);- 如果
state == 目标值(符合):跳出while循环,执行 ④ 业务逻辑; - 如果
state != 目标值(不符合):再次执行 ②(wait()),释放锁 + 重新阻塞 —— 相当于 “无效唤醒”,线程回到等待状态,不会改变任何值。
- 如果
核心:while 是 “循环判断”,唤醒后必须再查一次 state,避免 “唤醒但条件没满足” 的问题。
场景 2:wait() 不在 while 里
如果把 wait() 移出 while,比如:
synchronized (lock) {
if (state != 目标值) { // ① 只判断一次
lock.wait(); // ② 等待点:释放锁+阻塞
// ③ 唤醒后执行点
}
// ④ 业务逻辑(不管 state 对不对,都会执行)
}
- 线程被唤醒 → 抢锁 → 抢到后从 ③ 开始执行;
- 因为没有
while循环,执行完 ③ 后,不会回头判断任何条件,直接执行 ④ 业务逻辑; - 哪怕此时
state依然不符合目标值(比如虚假唤醒),也会执行业务逻辑,导致代码出错(比如交替打印时顺序混乱)。
场景3:wait前有while循环
synchronized (lock) {
// 第一块:普通循环(执行一次就结束)
while(count < 10) {
count++
}
// 第二块:state 判断+wait()
while (state != 目标值) {
lock.wait();
// ③ 唤醒后执行点
}
// ④ 业务逻辑
}
- 线程执行到第二块代码的
lock.wait()→ 释放锁、阻塞; - 被唤醒后 → 抢锁成功 → 从
lock.wait()的下一行(③)继续执行; - 执行完③后,因为是
while (state != 目标值)循环,会回头判断这个循环的条件(第二块代码的循环),而不是回头执行第一块代码的while(true); - 哪怕跳出第二块的
while循环,也会执行第四块业务逻辑,永远不会再回到第一块的while(true)。
关键结论:
- 如果
wait()不在while里,唤醒后不会再执行原来的条件判断(不管是while还是if); - 这也是为什么
wait()必须放在while循环内 —— 只有循环才能强制唤醒后重新判断条件,保证线程安全。
最终总结
- 只有包含
wait()的那个while循环,唤醒后才会重新执行(因为wait()在循环内,唤醒后会回到循环条件判断);前面不包含wait()的while循环,唤醒后绝对不会再执行(代码不回溯,执行过就过了)。
更多推荐


所有评论(0)