package org.example;
public class Main {
public static void main(String[] args) {
Cook c = new Cook();
Foodie f=new Foodie();
c.setName("厨师");
f.setName("吃货");
c.start();
f.start();
}
}
package org.example;
public class Desk {
//0:没有面条; 1:有面条
public static int foodFlag = 0;
//总个数
public static int count=10;
//锁对象
public static Object lock = new Object();
}
package org.example;
public class Cook extends Thread{
@Override
public void run() {
while (true){
synchronized (Desk.lock){
if(Desk.count ==0){
break;
}else{
//判断桌子上是否有食物
if(Desk.foodFlag ==1){
try{
Desk.lock.wait();
}
catch (InterruptedException e){
e.printStackTrace();
}
}else {
//如果没有,就制作食物
//System.out.println("厨师做了一碗面条");
//修改桌子上的食物状态
Desk.foodFlag = 1;
//叫醒等待的消费者开吃
Desk.lock.notifyAll();
}
}
}
System.out.println("厨师做了一碗面条");
}
}
}
package org.example;
public class Foodie extends Thread{
@Override
public void run() {
while (true){
synchronized (Desk.lock){
if(Desk.count==0){
break;
}else{
//先判断桌子上是否有面条
if (Desk.foodFlag ==0){
//如果没有,就等待
try {
Desk.lock.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}else{
Desk.count--;
//如果有,就开吃
System.out.println("吃货在吃面条,还能再吃"+ Desk.count+"碗!!!");
//log.info("吃货在吃面条,还能再吃"+ Desk.count+"碗!!!");
Desk.lock.notifyAll();
Desk.foodFlag =0;
}
}
}
}
}
}
厨师做了一碗面条
吃货在吃面条,还能再吃9碗!!!
厨师做了一碗面条
吃货在吃面条,还能再吃8碗!!!
厨师做了一碗面条
厨师做了一碗面条
吃货在吃面条,还能再吃7碗!!!
厨师做了一碗面条
厨师做了一碗面条
吃货在吃面条,还能再吃6碗!!!
厨师做了一碗面条
厨师做了一碗面条
吃货在吃面条,还能再吃5碗!!!
厨师做了一碗面条
厨师做了一碗面条
吃货在吃面条,还能再吃4碗!!!
厨师做了一碗面条
厨师做了一碗面条
吃货在吃面条,还能再吃3碗!!!
厨师做了一碗面条
吃货在吃面条,还能再吃2碗!!!
厨师做了一碗面条
厨师做了一碗面条
吃货在吃面条,还能再吃1碗!!!
厨师做了一碗面条
厨师做了一碗面条
吃货在吃面条,还能再吃0碗!!!
厨师做了一碗面条
您这次提供的代码中,Cook 和 Foodie 都使用了 if 而不是 while 来检查等待条件。运行结果中出现了大量连续的“厨师做了一碗面条”,这正是虚假唤醒 + if 条件判断导致的典型问题。
运行结果分析
-
吃货日志正好 10 条(从 9 碗到 0 碗),说明实际消费次数正确。
-
厨师日志远多于 10 条,且多次连续出现(例如“厨师做了一碗面条”连续两三条后才跟一条吃货日志)。
-
程序最终正常退出(
count减到 0 后两个线程都break退出)。
为什么会出现大量多余的厨师日志?
关键在于 Cook 类的代码结构:
java
synchronized (Desk.lock) {
if (Desk.count == 0) break;
else {
if (Desk.foodFlag == 1) {
Desk.lock.wait(); // 可能虚假唤醒
} else {
Desk.foodFlag = 1;
Desk.lock.notifyAll();
}
}
}
// 日志在同步块外部
System.out.println("厨师做了一碗面条");
虚假唤醒发生时:
-
厨师线程在
wait()时被 JVM 自发唤醒(没有notify)。 -
由于使用的是
if,线程不会重新检查foodFlag == 1是否仍然成立,直接退出if块。 -
然后退出同步块,必然执行
System.out.println("厨师做了一碗面条")。 -
但此时
foodFlag很可能还是 1(因为并没有真正的吃货来消费),厨师并没有真正做面条(没有执行else分支,foodFlag没有被再次设置为 1)。然而日志却打印了。
连续多次虚假唤醒:同一个厨师线程可能被连续多次虚假唤醒,每次都会打印一条日志,而 foodFlag 始终为 1,导致一连串的“厨师做了一碗面条”日志。直到某个时刻,吃货线程获得 CPU 时间,消费掉面条(foodFlag 变为 0,count 减 1,notifyAll),厨师才能进入正常的 else 分支真正做一次面条。
因此,厨师日志的数量 = 真正做面条的次数(10 次) + 虚假唤醒的次数。您的输出中厨师日志大约有 20 多条,说明发生了十几次虚假唤醒。
为什么使用 if 会导致这个问题?
-
if只检查一次条件。如果线程在条件不满足时wait,被唤醒后(无论是正常还是虚假)不会重新检查条件,直接继续执行后面的代码。 -
虚假唤醒使得线程在不满足条件的情况下继续执行,导致错误的日志打印(甚至更严重的状态错误)。
正确的做法(必须使用 while)
java
synchronized (Desk.lock) {
if (Desk.count == 0) break;
while (Desk.foodFlag == 1) { // while 循环
Desk.lock.wait();
}
// 此时 foodFlag 一定为 0
Desk.foodFlag = 1;
System.out.println("厨师做了一碗面条"); // 日志放在同步块内
Desk.lock.notifyAll();
}
同样,Foodie 类中也要改为:
java
while (Desk.foodFlag == 0) {
Desk.lock.wait();
}
使用 while 后:
-
每次唤醒(包括虚假唤醒)都会重新检查
foodFlag,如果不满足条件(例如还是 1)就继续wait,不会错误地打印日志或执行后续操作。 -
日志放在同步块内,保证与状态修改原子输出,不会出现多余的日志。
总结
-
您当前的
if写法没有防御虚假唤醒,导致厨师线程被多次虚假唤醒后错误地打印了多条“做面条”日志。 -
虽然程序没有崩溃(因为虚假唤醒没有修改共享数据),但日志完全不可信,且如果逻辑更复杂(例如在
wait()后直接修改状态)就会引发严重错误。 -
必须将
if改为while,这是 Java 并发编程的强制规范,不是可选项。
修复后,输出将会严格交替:
text
厨师做了一碗面条 吃货在吃面条,还能再吃9碗!!! 厨师做了一碗面条 吃货在吃面条,还能再吃8碗!!! ...

472

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



