【Java深入学习】wait和notify源码及易错点-下

94 阅读5分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

wait/notify的正确使用情况

既然是正确的使用情况,那就需要一步一步来,把不正确的部分逐渐优化。

现在有一群人需要干活,其中一个人叫做小南 他必须吸烟时才能干活。现在就是针对这个问题进行模拟。

模拟一:

import lombok.extern.slf4j.Slf4j;
@Slf4j(topic = "c.TestCorrectPosture")
public class TestCorrectPostureStep1 {
    static final Object room = new Object();
    static boolean hasCigarette = false; // 有没有烟
    static boolean hasTakeout = false;

    public static void main(String[] args) {
        new Thread(() -> {
            synchronized (room) {
                log.debug("有烟没?[{}]", hasCigarette);
                if (!hasCigarette) {
                    log.debug("没烟,先歇会!");
                    try {
                        Thread.sleep(2000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                log.debug("有烟没?[{}]", hasCigarette);
                if (hasCigarette) {
                    log.debug("可以开始干活了");
                }
            }
        }, "小南").start();

        for (int i = 0; i < 5; i++) {
            new Thread(() -> {
                synchronized (room) {
                    log.debug("可以开始干活了");
                }
            }, "其它人").start();
        }

       try {
           Thread.sleep(1000);
       } catch (InterruptedException e) {
           e.printStackTrace();
       }
        new Thread(() -> {
            // 这里能不能加 synchronized (room)?
            synchronized (room) { // 33行
                hasCigarette = true;
                log.debug("烟到了噢!");
            }
        }, "送烟的").start();
    }

}

结果:

33行加了synchronized:

  • 23:2516.146 c.TestCorrectPosture [小南] - 有烟没?[false]
  • 23:25:16.149 c.TestCorrectPosture [小南] - 没烟,先歇会!
  • 23:25:18.157 c.TestCorrectPosture [小南] - 有烟没?[false]
  • 23:25:18.157 c.TestCorrectPosture [送烟的] - 烟到了噢!
  • 23:25:18.157 c.TestCorrectPosture [其它人] - 可以开始干活了
  • 23:25:18.157 c.TestCorrectPosture [其它人] - 可以开始干活了
  • 23:25:18.158 c.TestCorrectPosture [其它人] - 可以开始干活了
  • 23:25:18.158 c.TestCorrectPosture [其它人] - 可以开始干活了
  • 23:25:18.158 c.TestCorrectPosture [其它人] - 可以开始干活了

33行不加synchronized:

  • 23:26:19.464 c.TestCorrectPosture [小南] - 有烟没?[false]
  • 23:26:19.468 c.TestCorrectPosture [小南] - 没烟,先歇会!
  • 23:26:20.475 c.TestCorrectPosture [送烟的] - 烟到了噢!
  • 23:26:21.470 c.TestCorrectPosture [小南] - 有烟没?[true]
  • 23:26:21.470 c.TestCorrectPosture [小南] - 可以开始干活了
  • 23:26:21.471 c.TestCorrectPosture [其它人] - 可以开始干活了
  • 23:26:21.471 c.TestCorrectPosture [其它人] - 可以开始干活了
  • 23:26:21.471 c.TestCorrectPosture [其它人] - 可以开始干活了
  • 23:26:21.471 c.TestCorrectPosture [其它人] - 可以开始干活了
  • 23:26:21.471 c.TestCorrectPosture [其它人] - 可以开始干活了

解释:

33行不加synchronized:

当我们不加synchronized时我们发现的问题是 由于小南调用了sleep 睡眠2s期间没有释放锁 所以此时其他线程加锁的代码块不能运行,导致其他人没有办法工作

33行加synchronized:

当加synchronize的时上述问题依旧没有解决,且出现一个新的问题,小南在sleep 2s期间 主线程的第33行因为加了synchronized导致hasCigarette并没有改变为true,所以此时小南在1s后没有收到烟 所以小南没有工作

模拟二

我们在之前的题目上再加一个 人物 小女,小女需要外卖才能工作

@Slf4j(topic = "c.TestCorrectPosture")
public class TestCorrectPostureStep3 {
    static final Object room = new Object();
    static boolean hasCigarette = false;
    static boolean hasTakeout = false;

    // 虚假唤醒
    public static void main(String[] args) {
        new Thread(() -> {
            synchronized (room) {
                log.debug("有烟没?[{}]", hasCigarette);
                if (!hasCigarette) {
                    log.debug("没烟,先歇会!");
                    try {
                        room.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                log.debug("有烟没?[{}]", hasCigarette);
                if (hasCigarette) {
                    log.debug("可以开始干活了");
                } else {
                    log.debug("没干成活...");
                }
            }
        }, "小南").start();

        new Thread(() -> {
            synchronized (room) {
                Thread thread = Thread.currentThread();
                log.debug("外卖送到没?[{}]", hasTakeout);
                if (!hasTakeout) {
                    log.debug("没外卖,先歇会!");
                    try {
                        room.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                log.debug("外卖送到没?[{}]", hasTakeout);
                if (hasTakeout) {
                    log.debug("可以开始干活了");
                } else {
                    log.debug("没干成活...");
                }
            }
        }, "小女").start();

        try {
           Thread.sleep(1000);
       } catch (InterruptedException e) {
           e.printStackTrace();
       }
        new Thread(() -> {
            synchronized (room) {
                hasTakeout = true;
                log.debug("外卖到了噢!");
                room.notify();
            }
        }, "送外卖的").start();
    }

}

结果:

  • 23:45:35.476 c.TestCorrectPosture [小南] - 有烟没?[false]
  • 23:45:35.486 c.TestCorrectPosture [小南] - 没烟,先歇会!
  • 23:45:35.486 c.TestCorrectPosture [小女] - 外卖送到没?[false]
  • 23:45:35.486 c.TestCorrectPosture [小女] - 没外卖,先歇会!
  • 23:45:36.483 c.TestCorrectPosture [送外卖的] - 外卖到了噢!
  • 23:45:36.483 c.TestCorrectPosture [小南] - 有烟没?[false]
  • 23:45:36.483 c.TestCorrectPosture [小南] - 没干成活…

解释:

我们发现在新增一个小女 之后 这个代码又出现了问题,刚开始 小南 小女条件都不满足不能工作,但是因为notify是唤醒某一个线程,导致 本来应该唤醒小女的线程 把小南唤醒了 但是没有给小南需要的条件,而且小女也因此没有机会活动外卖,导致小女与小女都没有干活

这种把不该唤醒的线程唤醒的情况叫做虚假唤醒

模拟三

@Slf4j(topic = "c.TestCorrectPosture")
public class TestCorrectPostureStep4 {
    static final Object room = new Object();
    static boolean hasCigarette = false;
    static boolean hasTakeout = false;

    public static void main(String[] args) {


        new Thread(() -> {
            synchronized (room) {
                log.debug("有烟没?[{}]", hasCigarette);
                if (!hasCigarette) {
                    log.debug("没烟,先歇会!");
                    try {
                        room.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                log.debug("有烟没?[{}]", hasCigarette);
                if (hasCigarette) {
                    log.debug("可以开始干活了");
                } else {
                    log.debug("没干成活...");
                }
            }
        }, "小南").start();

        new Thread(() -> {
            synchronized (room) {
                Thread thread = Thread.currentThread();
                log.debug("外卖送到没?[{}]", hasTakeout);
                if (!hasTakeout) {
                    log.debug("没外卖,先歇会!");
                    try {
                        room.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                log.debug("外卖送到没?[{}]", hasTakeout);
                if (hasTakeout) {
                    log.debug("可以开始干活了");
                } else {
                    log.debug("没干成活...");
                }
            }
        }, "小女").start();

       try {
           Thread.sleep(1000);
       } catch (InterruptedException e) {
           e.printStackTrace();
       }
        new Thread(() -> {
            synchronized (room) {
                hasTakeout = true;
                log.debug("外卖到了噢!");
                room.notifyAll();
            }
        }, "送外卖的").start();


    }

}

结果:

  • 00:00:01.274 c.TestCorrectPosture [小南] - 有烟没?[false]
  • 00:00:01.274 c.TestCorrectPosture [小南] - 没烟,先歇会!
  • 00:00:01.274 c.TestCorrectPosture [小女] - 外卖送到没?[false]
  • 00:00:01.274 c.TestCorrectPosture [小女] - 没外卖,先歇会!
  • 00:00:02.284 c.TestCorrectPosture [送外卖的] - 外卖到了噢!
  • 00:00:02.284 c.TestCorrectPosture [小女] - 外卖送到没?[true]
  • 00:00:02.284 c.TestCorrectPosture [小女] - 可以开始干活了
  • 00:00:02.284 c.TestCorrectPosture [小南] - 有烟没?[false]
  • 00:00:02.284 c.TestCorrectPosture [小南] - 没干成活…

解释:

我们这次把notify换成了notifyAll,使得小女一定可以被唤醒 并且收到外卖。事实也是如此,小女获得了外卖 并且开始工作,但是小南依旧被唤醒了 并且没有收到烟 导致小南没有干活。

现在的情况是 虽然唤醒了应该唤醒的 小女线程,但是小南线程还是被错误唤醒了,依旧是虚假唤醒

建议使用的格式

synchronized(lock){
    while(条件不成立){
        lock.wait();
    }
    // 后续代码
}

// 另一个线程
synchronized(lock){
    lock.notifyAll();
}

这样的写法避免了 虚假唤醒的情况 也保证了 唤醒的线程一定可以获得需要的条件 从而进行工作。