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

简介: 【多线程:wait/notify详解】原理及错误用法(虚假唤醒等)

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

01.介绍

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

02.工作原理

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

202207142248861.png

上图为它的工作原理

注意

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();
}

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

目录
相关文章
|
9天前
|
安全 Java 开发者
深入解读JAVA多线程:wait()、notify()、notifyAll()的奥秘
在Java多线程编程中,`wait()`、`notify()`和`notifyAll()`方法是实现线程间通信和同步的关键机制。这些方法定义在`java.lang.Object`类中,每个Java对象都可以作为线程间通信的媒介。本文将详细解析这三个方法的使用方法和最佳实践,帮助开发者更高效地进行多线程编程。 示例代码展示了如何在同步方法中使用这些方法,确保线程安全和高效的通信。
32 9
|
12天前
|
Java
JAVA多线程通信:为何wait()与notify()如此重要?
在Java多线程编程中,`wait()` 和 `notify()/notifyAll()` 方法是实现线程间通信的核心机制。它们通过基于锁的方式,使线程在条件不满足时进入休眠状态,并在条件满足时被唤醒,从而确保数据一致性和同步。相比其他通信方式,如忙等待,这些方法更高效灵活。 示例代码展示了如何在生产者-消费者模型中使用这些方法实现线程间的协调和同步。
26 3
|
27天前
|
安全 Java
Java多线程通信新解:本文通过生产者-消费者模型案例,深入解析wait()、notify()、notifyAll()方法的实用技巧
【10月更文挑战第20天】Java多线程通信新解:本文通过生产者-消费者模型案例,深入解析wait()、notify()、notifyAll()方法的实用技巧,包括避免在循环外调用wait()、优先使用notifyAll()、确保线程安全及处理InterruptedException等,帮助读者更好地掌握这些方法的应用。
17 1
|
27天前
|
Java
在Java多线程编程中,`wait()`和`notify()`方法的相遇如同一场奇妙的邂逅
在Java多线程编程中,`wait()`和`notify()`方法的相遇如同一场奇妙的邂逅。它们用于线程间通信,使线程能够协作完成任务。通过这些方法,生产者和消费者线程可以高效地管理共享资源,确保程序的有序运行。正确使用这些方法需要遵循同步规则,避免虚假唤醒等问题。示例代码展示了如何在生产者-消费者模型中使用`wait()`和`notify()`。
25 1
|
27天前
|
安全 Java 开发者
Java多线程中的`wait()`、`notify()`和`notifyAll()`方法,探讨了它们在实现线程间通信和同步中的关键作用
本文深入解析了Java多线程中的`wait()`、`notify()`和`notifyAll()`方法,探讨了它们在实现线程间通信和同步中的关键作用。通过示例代码展示了如何正确使用这些方法,并分享了最佳实践,帮助开发者避免常见陷阱,提高多线程程序的稳定性和效率。
34 1
|
1月前
|
存储 消息中间件 资源调度
C++ 多线程之初识多线程
这篇文章介绍了C++多线程的基本概念,包括进程和线程的定义、并发的实现方式,以及如何在C++中创建和管理线程,包括使用`std::thread`库、线程的join和detach方法,并通过示例代码展示了如何创建和使用多线程。
44 1
C++ 多线程之初识多线程
|
27天前
|
Java 开发者
在Java多线程编程中,创建线程的方法有两种:继承Thread类和实现Runnable接口
【10月更文挑战第20天】在Java多线程编程中,创建线程的方法有两种:继承Thread类和实现Runnable接口。本文揭示了这两种方式的微妙差异和潜在陷阱,帮助你更好地理解和选择适合项目需求的线程创建方式。
19 3
|
27天前
|
Java 开发者
在Java多线程编程中,选择合适的线程创建方法至关重要
【10月更文挑战第20天】在Java多线程编程中,选择合适的线程创建方法至关重要。本文通过案例分析,探讨了继承Thread类和实现Runnable接口两种方法的优缺点及适用场景,帮助开发者做出明智的选择。
17 2
|
27天前
|
Java
Java中多线程编程的基本概念和创建线程的两种主要方式:继承Thread类和实现Runnable接口
【10月更文挑战第20天】《JAVA多线程深度解析:线程的创建之路》介绍了Java中多线程编程的基本概念和创建线程的两种主要方式:继承Thread类和实现Runnable接口。文章详细讲解了每种方式的实现方法、优缺点及适用场景,帮助读者更好地理解和掌握多线程编程技术,为复杂任务的高效处理奠定基础。
29 2
|
27天前
|
Java 开发者
Java多线程初学者指南:介绍通过继承Thread类与实现Runnable接口两种方式创建线程的方法及其优缺点
【10月更文挑战第20天】Java多线程初学者指南:介绍通过继承Thread类与实现Runnable接口两种方式创建线程的方法及其优缺点,重点解析为何实现Runnable接口更具灵活性、资源共享及易于管理的优势。
33 1