一、 死锁的核心条件

  1. 互斥条件:资源只能被一个线程独占使用,其他线程需等待直到资源释放。
  2. 请求与保持条件:线程已持有部分资源,又提出新的资源请求,且不释放已持有的资源。
  3. 不可剥夺条件:线程已获得的资源不能被强制剥夺,只能由线程主动释放。
  4. 循环等待条件:多个线程形成环形链,每个线程都在等待链中下一个线程所持有的资源

死锁避免方法

  1. 打破 “嵌套锁” 改为 “并列锁”

    • 原理:破坏 “请求与保持” 条件。原本线程在持有一把锁的同时请求另一把锁(嵌套),改为同时申请多把锁(并列),避免部分持有资源后再请求新资源的情况。
    • 示例:若需同时操作lock1lock2,可设计一个方法同时获取这两把锁,而非先拿lock1再拿lock2
  2. 对加锁顺序做出约定

    • 原理:破坏 “循环等待” 条件。所有线程严格按照约定的同一顺序(如先lock1lock2)获取锁,避免形成环形等待链。
    • 示例:线程 A 和线程 B 都需操作lock1lock2,则统一约定先获取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()只 “叫醒” 不 “让权” → 唤醒线程执行完同步代码块、释放锁后 → 被唤醒的线程才重新竞争锁 → 竞争到锁才能继续执行后续代码(竞争锁时可能短暂阻塞)。

关键细节:

  1. 唤醒线程的 “拿锁前提”:必须先占锁,才能唤醒没错!notify()的本质是 “通知持有同一把锁的等待线程”,而要调用notify(),前提是当前线程已经通过synchronized (lock)拿到了这把锁 —— 如果没拿锁就调用lock.notify(),会直接抛出IllegalMonitorStateException异常(之前说的 “wait()/notify()必须搭配synchronized”,本质就是这个要求)。

  2. 被唤醒线程的 “阻塞场景”:只在 “竞争锁” 时可能阻塞,不是额外等待你说的 “唤醒之后还得阻塞等待一会”,准确来说是:

    • notify()后,被唤醒的线程会从 “等待队列” 转到 “锁的竞争队列”(和其他可能也在抢这把锁的线程一起排队);
    • 只有当唤醒线程退出同步代码块、释放锁后,竞争队列里的线程才会抢锁;
    • 如果此时没有其他线程抢锁,被唤醒的线程会直接拿到锁,立刻继续执行wait()后面的代码(几乎没有阻塞);
    • 如果有其他线程也在抢这把锁,就会短暂阻塞,直到抢到锁为止。

    简单说:唤醒后不是 “固定阻塞一会”,而是 “按需竞争锁”—— 有竞争就等,没竞争就直接执行。

  • 同步代码块 = 带锁的房间,lock = 房间钥匙;
  • 等待线程:先拿到钥匙(进入同步代码块)→ 喊 “我先等会,钥匙先还回去”(wait()释放锁,进等待队列)→ 站在门外等通知;
  • 唤醒线程:先拿到同一把钥匙(进入同步代码块)→ 喊 “里面那个等的可以进来了”(notify())→ 继续在房间里做完自己的事(执行同步代码块剩余逻辑)→ 出门还钥匙(释放锁);
  • 等待线程:听到通知后,跑到门口和其他想进房间的线程一起抢钥匙(竞争锁)→ 抢到钥匙就进房间,继续做之前没做完的事(wait()后面的代码)→ 做完出门还钥匙。

5.notifyAll()⽅法

notify⽅法只是唤醒某⼀个等待线程.使⽤notifyAll⽅法可以⼀次唤醒所有的等待线程.

6.补充问题疑惑区:lock.wait() 唤醒后流程:

lock.wait() 唤醒后流程:唤醒→自动抢锁→抢到后从 wait() 下一行执行→while 循环回头判断 state→符合则执行业务,不符合则再次 wait()

 唤醒后抢锁成功,直接从 wait() 下一行执行,while 会强制回头再判断 stateif 不会,这就是两者的本质区别。

synchronized (lock) {
    while (state != 目标值) { // 唤醒后必走这行(因为循环要回头判断)
        lock.wait(); // 唤醒后从这行的下一行开始执行
        // 👇 唤醒后先执行这里的代码,再回到上面的 while 条件判断
    }
    // 业务逻辑
}
synchronized (lock) {
    if (state != 目标值) { // 唤醒后不走这行(因为 if 只判断一次)
        lock.wait(); // 唤醒后从这行的下一行开始执行
        // 👇 唤醒后直接执行这里的代码,不会回头判断 if
    }
    // 业务逻辑
}

关键细节(避免踩坑)

  1. “执行 synchronized (lock)” 是抢锁结果,不是主动执行代码:你认为 “底层自动实现 执行 synchronized (lock) {”,更准确的是:唤醒后的线程会自动参与抢锁,抢到锁后,才能进入 synchronized 同步块,继续执行 wait() 后面的代码 —— 不是 “主动执行 synchronized 这行代码”,而是抢锁成功后的 “进入权限”。

  2. while 的 “必须执行” 是循环特性,不是额外逻辑:不是 “因为写了 while 就必须多执行一次”,而是 wait() 唤醒后,代码从 wait() 下一行继续走,走到 while 循环的末尾,会自动回头判断循环条件(这是 while 本身的循环逻辑)—— 相当于 “自然回头查一下 state 对不对”。

  3. if 不是 “不用执行”,是 “执行过一次就不回头”:if 也会执行一次判断(在 wait() 之前),但唤醒后,代码不会回头再执行 if 判断 —— 相当于 “查过一次就不管了,直接往下走”。

场景 1:wait() 在 while 循环内

先再明确代码执行流程:

synchronized (lock) {
    while (state != 目标值) { // ① 条件判断点
        lock.wait(); // ② 等待点:释放锁+阻塞
        // ③ 唤醒后执行点:wait() 的下一行
    }
    // ④ 业务逻辑
}
唤醒后的完整流程:
  1. 线程被唤醒 → 自动抢锁 → 抢到锁后,从 ③ 开始执行wait() 的下一行);
  2. 执行完 ③ 后,因为是 while 循环,会自动回头执行 ①(条件判断)
    • 如果 state == 目标值(符合):跳出 while 循环,执行 ④ 业务逻辑;
    • 如果 state != 目标值(不符合):再次执行 ②(wait()),释放锁 + 重新阻塞 —— 相当于 “无效唤醒”,线程回到等待状态,不会改变任何值。
核心:while 是 “循环判断”,唤醒后必须再查一次 state,避免 “唤醒但条件没满足” 的问题。

场景 2:wait() 不在 while 里

如果把 wait() 移出 while,比如:

synchronized (lock) {
    if (state != 目标值) { // ① 只判断一次
        lock.wait(); // ② 等待点:释放锁+阻塞
        // ③ 唤醒后执行点
    }
    // ④ 业务逻辑(不管 state 对不对,都会执行)
}
  1. 线程被唤醒 → 抢锁 → 抢到后从 ③ 开始执行;
  2. 因为没有 while 循环,执行完 ③ 后,不会回头判断任何条件,直接执行 ④ 业务逻辑;
  3. 哪怕此时 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 循环,唤醒后绝对不会再执行(代码不回溯,执行过就过了)
Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐