多线程——wait 和 notify
目录
5.3 wait() 和 join()、sleep() 的区别汇总:
1.wait 和 notify 引入
在学习多线程,我们多次强调线程是随机调度、抢占式执行的,为此我们就针对这句话学习了很多关于多线程的知识。针对线程等待引入 join() 方法、线程睡眠引入 sleep() 方法,线程安全原子性问题引入关键字 synchronized ,线程安全可见性问题和指令重排序问题引入关键字 volatile 等等。可是不论是方法也好,关键字也罢,因为这些线程是随机调度、抢占式执行的,导致它们都有一个缺点,就是当第一个线程执行后,第二个线程就无法再次执行,只能阻塞等待,直到第一个线程执行完毕。如果现在就这么一个需求:第一个线程执行一段时间,但是并没有执行结束,此时它就想“摆烂”一会,让第二个线程先执行工作,等再过一段时间,第一个线程再执行,怎么解决这个办法呢?
比如:你进一家餐馆就餐,你要通知服务员进行点单,你点完单后就要等待,而这时候服务员就要通知后厨,等餐点准备好后,服务员会通知你,你的餐准备好了,这个时候你就可以就餐了。
很显然,对于这样的需求,根据过往所学的多线程知识是解决不了的。因为根据过往的多线程知识,“你”就相当于一个线程,想让“服务员”这个线程执行,那么就说明“你”这个线程已经执行结束了,虽然现实生活你肯定会等到餐点,但是计算机的线程已经执行完毕,start() 只开启一次,执行结束就不再可以执行。
所以,针对这类问题,Java中提供了等待方法 wait(),唤醒方法 notify(),这两个方法都属于 Object 类,并且要搭配锁使用。这些方法在使用时被这个锁调用,使用时也会抛出 InterruptedException,所以要事先声明。
2.whit() 方法
wait():让当前线程进入等待状态,使用时要在同步方法块(synchronized)内使用,否则会抛出 IllegalMonitorStateException 异常,并且要求 wait 锁对象和 synchronized 锁对象一致。
先来看一段代码:
public class Test {
public static void main(String[] args) throws InterruptedException {
Object locker = new Object();
synchronized (locker) {
System.out.println("wait 之前...");
locker.wait();
System.out.println("wait 之后...");
}
}
}
代码中定义了一个锁,并且在锁中调用 wait() 方法,如果运行这个代码,会一直等待,怎么证明?我们借助 jconsole 查看状态:

当前代码运行的是在 main 线程,可以看到该线程状态是 WAITING 。
但是如果一个线程没有另一个线程来唤醒时,是不是意味着一直阻塞等待,导致死等?所以在这里 wait() 方法也提供了带参数的版本,即 wait(时间) ,单位是 ms,超过这个时间还没有其它线程来唤醒时,会继续往下执行。这里不再进行演示。
wait 要做的事情就是:
- 执行当前代码的线程进入等待状态
- 释放当前拥有的锁
- 满足一定条件时被唤醒,再次尝试获取锁
wait 等待结束的条件:
- 有其他线程调用该对象的 notify() 方法
- 如果 wait() 方法带有时间参数,那么时间超时也会结束等待
- 其它线程调用该线程的 interrupted 方法,相当于中断这个线程
3.notify() 方法
notify():唤醒当前对象的等待线程,使用时也要在同步方法块(synchronized)内使用,否则会抛出 InterruptedException 异常。如果有多个线程在等待当前线程唤醒,根据线程随机调度会随机唤醒一个正在 WAITING 的线程。notify() 持有的锁对象要和 wait() 一致。
这里来定义三个线程,让两个线程进入 WAITING 状态,一个线程来唤醒,感受一下 :
public class Test {
public static void main(String[] args) throws InterruptedException {
Object locker = new Object();
Thread t1 = new Thread(() -> {
synchronized (locker) {
try {
System.out.println("线程1wait之前...");
locker.wait();//进入WAITING状态
System.out.println("线程1wait之后...");
}catch (InterruptedException e){
e.printStackTrace();
}
}
},"等待线程1");
Thread t2 = new Thread(() -> {
synchronized (locker) {
try {
System.out.println("线程2wait之前...");
locker.wait();//进入WAITING状态
System.out.println("线程2wait之后...");
}catch (InterruptedException e){
e.printStackTrace();
}
}
},"等待线程2");
Thread t3 = new Thread(() -> {
synchronized (locker) {
System.out.println("唤醒线程notify之前");
locker.notify();
System.out.println("唤醒线程notify之后");
}
},"唤醒线程");
//开启线程
t1.start();
t2.start();
Thread.sleep(1000);//为了更好地看到现象这里休眠1秒,目的先让等待线程执行
t3.start();
}
}
代码解读:wait() 和 notify() 都要被同一个锁对象调用,并且两个等待线程中的锁对象也是同一个,这样当唤醒线程执行后,会随机唤醒一个等待线程。
运行结果:

结果分析:线程1比线程2先执行,这是由于线程的随机调度导致,等待1秒后,唤醒线程执行,从结果来看,此时是唤醒线程1,因为打印了“线程1wait 之后”,线程2发生阻塞等待,进入WAITING状态,在这里同样借助 jconsole 查看状态:

可以看到,在线程中,等待线程2依旧处于WAITING状态,而等待线程1、唤醒线程和main线程已经执行完毕,所以并没有看到这两个线程。
有没有声明办法一次唤醒所有线程呢?肯定是有的,那就是 notifyAll() 方法,上述代码中,只要把 notify() 方法改成 notifyAll() 就可以解决,使用这个方法,持有当前锁对象正在 WAITING 的线程由于随机调度也会随机执行,比如(下面的输出结果是把 notify() 方法换成了 notifyAll(),其它的都没有改变):

等待之前是线程1先执行,而唤醒之后是线程2先执行,这又相当于对线程的随机调度、抢占式执行的再次理解!
4.wait() 和 notify() 注意点
关于wait() 和 notify() 注意点在上面介绍都有讲到,但是这里也统一罗列出来:
- wait() 和 notify() 属于Object类
- wait() 必须满足锁状态,这个方法需要释放锁,并且 wait() 要在 notify() 之前,这样正在 WAITING 状态的线程才会被唤醒,如果 notify() 在 wait() 之前,虽然没有副作用(不会抛异常之类的),但是会导致后面 WAITING 的线程一致等待
- wait(), notify() 和 synchronized 的锁对象必须一致,在使用时要注意区分
- 一次 notify 只能唤醒一个 wait 对象,这是因为线程的随机调度、抢占式执行导致
- 调用 wait() 时,相当于相当于获取锁状态,调用 notify() 后,因为是在 synchronized内,所以这个线程不会立即结束,而是等待唤醒线程即将结束后才会解锁(这个期间相当于等待线程在抢占锁),此时 wait() 解锁,继续执行 wait() 之后的代码
5.wait() 和 join()、sleep() 的区别
5.1 wait() 和 join() 的区别
wait() 和 join() 都表示等待的意思,并且也提供来“死等”版本和“超时”版本“。它们有什么区别呢?
(1)join() 的底层实现是 wait
这里可以打开 join() 方法的源码查看:

(2)wait() 所属类是 Object 类,join() 所属类是 Thread 类
(3)wait() 可以被 notify() 唤醒,也可以等待超时后执行当前等待的线程,join()需要等待上一个线程执行结束或超时,才能被唤醒执行下一个线程
(4)wait() 必须搭配锁使用,代码要在 synchronized 代码块内,并且 wait() 和 notify() 持有的锁对象要一致,join() 的底层是 wait() ,内部通过 wait() 释放锁(这就是为什么源码 join() 方法被 synchronized 修饰的原因),而不会直接暴露锁机制
5.2 wait() 和 join() 的区别
wait() 引入带有时间参数的版本后,和 sleep() 非常像,都可以表示等待一段的意思。它们有什么区别呢?
(1)wait() 所属类是 Object 类,不是静态方法,sleep() 所属类是 Thread 类,是静态方法,调用方式是 Thread.sleep(时间)
(2)wait() 可以被 notify() 唤醒,也可以等待超时后执行当前等待的线程,sleep() 会让当前线程短暂睡眠指定时间,时间到期后才唤醒
(3)wait() 必须搭配锁使用,代码要在 synchronized 代码块内,并且 wait() 和 notify() 持有的锁对象要一致,sleep() 不需要搭配锁
(4)wait() 位置必须在 synchronized 代码块内,sleep() 可以是线程内的任意位置
5.3 wait() 和 join()、sleep() 的区别汇总:

关于多线程的基础知识学了这么久,虽然有结合生活案例来理解,但是多线程在具体实际中的应用模式是怎么样的呢?使用多线程怎么考虑线程安全?
欲知后事如何,且听下回分解!
更多推荐


所有评论(0)