【面试:并发篇16:多线程:wait/notify详解】原理及错误用法(虚假唤醒等)

发布时间:2022-12-13 12:30

【面试:并发篇16:多线程:wait/notify详解】原理及错误用法(虚假唤醒等)

00.前言

如果有任何问题,请指出。

01.介绍

我们之前学习的过程中浅显的了解过wiat/notify,但是没有系统的介绍过wait/notify,wait是使线程陷入等待 notify是随机唤醒一个被wait的线程。

02.工作原理

当一个线程获取锁后 但是发现自己不满足某些条件 不能执行锁住部分的代码块时 需要进入等待列表 直到满足条件时才会重新竞争线程

【面试:并发篇16:多线程:wait/notify详解】原理及错误用法(虚假唤醒等)_第1张图片

上图为它的工作原理

注意

1.Owner发现条件某个线程不满足条件,调用wait方法,此时这个线程进入WaitSet,并且这个线程的状态变为WAITING状态

2.BLOCKED和WAITING状态的线程都不参与cpu调度,不占用cpu时间片

3.WAITING线程会在Owner线程调用notify或notifyAll时唤醒,但唤醒后仍然要进入EntryList重新竞争锁

03.API介绍

obj.wait():wait方法让进入object监视器的线程到waitSet等待。wait后会释放对象锁,让其他线程竞争

obj.wait(Long timeout):wait的有时限方法,如果在时限内没有其他线程唤醒,则自己直接唤醒自己,若期间有别的线程唤醒那就正常唤醒。wait后会释放对象锁,让其他线程竞争

obj.notify():notify方法让正在waitSet等待的线程挑一个唤醒

obj.notifyAll():notifyAll方法让正在waitSet等待的线程全部唤醒

它们都是线程之间进行协作的手段,都属于Object对象方法,必须获取此对象的锁,才能调用这几个方法,如果不加锁直接调用这些方法会报错

notify与notifyAll的对比

@Slf4j(topic = "c.TestWaitNotify")
public class TestWaitNotify {
    final static Object obj = new Object();

    public static void main(String[] args) {

        new Thread(() -> {
            synchronized (obj) {
                log.debug("执行....");
                try {
                    obj.wait(); // 让线程在obj上一直等待下去
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                log.debug("其它代码....");
            }
        },"t1").start();

        new Thread(() -> {
            synchronized (obj) {
                log.debug("执行....");
                try {
                    obj.wait(); // 让线程在obj上一直等待下去
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                log.debug("其它代码....");
            }
        },"t2").start();

        // 主线程两秒后执行
        sleep(0.5);
        log.debug("唤醒 obj 上其它线程");
        synchronized (obj) {
//            obj.notify(); // 唤醒obj上一个线程
            obj.notifyAll(); // 唤醒obj上所有等待线程
        }
    }
}

结果

调用notify时:

23:11:55.798 c.TestWaitNotify [t1] - 执行…
23:11:55.801 c.TestWaitNotify [t2] - 执行…
23:11:56.300 c.TestWaitNotify [main] - 唤醒 obj 上其它线程
23:11:56.300 c.TestWaitNotify [t1] - 其它代码…

调用notifyAll时:

23:12:26.195 c.TestWaitNotify [t1] - 执行…
23:12:26.198 c.TestWaitNotify [t2] - 执行…
23:12:26.699 c.TestWaitNotify [main] - 唤醒 obj 上其它线程
23:12:26.699 c.TestWaitNotify [t2] - 其它代码…
23:12:26.699 c.TestWaitNotify [t1] - 其它代码…

解释

可以看出notify是随机唤醒一个线程,notifyAll则是唤醒全部线程

04.wait与sleep方法的区别

区别

1.sleep是Thread的类方法,而wait是Object的对象方法

2.sleep不需要强制和synchronized配合使用,但是wait需要和synchronized一起用

3.sleep在睡眠的同时,不会释放对象锁,但wait在等待时会释放对象锁

4.无时限wait方法执行后 线程变为WAITING状态,有时限的wait方法与sleep方法执行后变为TIMED_WAITING状态

分析下面代码

@Slf4j(topic = "c.Test19")
public class Test19 {

    static final Object lock = new Object();
    public static void main(String[] args) {
        new Thread(() -> {
            synchronized (lock) {
                log.debug("获得锁");
                try {
//                    Thread.sleep(2000);
                    lock.wait(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "t1").start();

        Sleeper.sleep(1);
        synchronized (lock) {
            log.debug("获得锁");
        }
    }
}

结果

当调用sleep时的情况:

23:20:48.788 c.Test19 [t1] - 获得锁

当调用wait时的情况:

23:21:27.759 c.Test19 [t1] - 获得锁
23:21:28.768 c.Test19 [main] - 获得锁

解释

上述结果说明sleep在暂停期间 不会释放锁 导致 这期间其他线程不能运行,而wait则可以释放锁

05.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后没有收到烟 所以小南没有工作

模拟二

可以看出模拟一的主要问题是 小南不干活 其他人也要等,而且小南有可能会由于送烟代码块被加锁 导致收不到烟不干活

我们用wait/notify来优化

@Slf4j(topic = "c.TestCorrectPosture")
public class TestCorrectPostureStep2 {
    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(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) {
                hasCigarette = true;
                log.debug("烟到了噢!");
                room.notify();
            }
        }, "送烟的").start();
    }
}

结果

23:30:41.080 c.TestCorrectPosture [小南] - 有烟没?[false]
23:30:41.083 c.TestCorrectPosture [小南] - 没烟,先歇会!
23:30:41.084 c.TestCorrectPosture [其它人] - 可以开始干活了
23:30:41.084 c.TestCorrectPosture [其它人] - 可以开始干活了
23:30:41.084 c.TestCorrectPosture [其它人] - 可以开始干活了
23:30:41.085 c.TestCorrectPosture [其它人] - 可以开始干活了
23:30:41.085 c.TestCorrectPosture [其它人] - 可以开始干活了
23:30:42.085 c.TestCorrectPosture [送烟的] - 烟到了噢!
23:30:42.085 c.TestCorrectPosture [小南] - 有烟没?[true]
23:30:42.086 c.TestCorrectPosture [小南] - 可以开始干活了

看起来好像没有问题了,但是如果此时还有另外一个需要条件的才能工作的线程呢?

模拟三

我们在之前的题目上再加一个 人物 小女,小女需要外卖才能工作,此时我们再来用模拟二的方法进行模拟

@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,使得小女一定可以被唤醒 并且收到外卖。事实也是如此,小女获得了外卖 并且开始工作,但是小南依旧被唤醒了 并且没有收到烟 导致小南没有干活。

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

模拟五

@Slf4j(topic = "c.TestCorrectPosture")
public class TestCorrectPostureStep5 {
    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);
                while (!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);
                while (!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:10:36.991 c.TestCorrectPosture [小南] - 有烟没?[false]
00:10:36.991 c.TestCorrectPosture [小南] - 没烟,先歇会!
00:10:36.991 c.TestCorrectPosture [小女] - 外卖送到没?[false]
00:10:36.991 c.TestCorrectPosture [小女] - 没外卖,先歇会!
00:10:37.990 c.TestCorrectPosture [送外卖的] - 外卖到了噢!
00:10:37.990 c.TestCorrectPosture [小女] - 外卖送到没?[true]
00:10:37.990 c.TestCorrectPosture [小女] - 可以开始干活了
00:10:37.990 c.TestCorrectPosture [小南] - 没烟,先歇会!

解释

这里我们用了一个很巧妙的处理 解决了模拟四种小南被虚假唤醒的情况,我们这里把小南的if判断改为while,使得如果判断失败 会再次循环 执行wait 进入WaitSet

06.wait/notify建议使用的格式

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

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

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

ItVuer - 免责声明 - 关于我们 - 联系我们

本网站信息来源于互联网,如有侵权请联系:561261067@qq.com

桂ICP备16001015号