Java多线程wait()和notify()方法详细图解
作者:Killing?Vibe 发布时间:2021-09-19 20:27:32
一、线程间等待与唤醒机制
wait()和notify()是Object类的方法,用于线程的等待与唤醒,必须搭配synchronized 锁来使用。
多线程并发的场景下,有时需要某些线程先执行,这些线程执行结束后其他线程再继续执行。
比如: 一个长跑比赛,裁判员要等跑步运动员冲线了才能宣判比赛结束,那裁判员线程就得等待所有的运动员线程运行结束后,再唤醒这个裁判线程。
二、等待方法wait()
wait 做的事情:
使当前执行代码的线程进行等待. (把线程放到等待队列中)
释放当前的锁
满足一定条件时被唤醒, 重新尝试获取这个锁.
wait 要搭配 synchronized 来使用. 脱离 synchronized 使用 wait 会直接抛出异常.
wait 结束等待的条件:
其他线程调用该对象的 notify 方法.
wait 等待时间超时 (wait 方法提供一个带有 timeout 参数的版本, 来指定等待时间).
其他线程调用该等待线程的 interrupted 方法, 导致 wait 抛出 InterruptedException 异常.
注意事项:
调用wait()方法的前提是首先要获取该对象的锁(synchronize对象锁)
调用wait()方法会释放锁,本线程进入等待队列等待被唤醒,被唤醒后不是立即恢复执行,而是进入阻塞队列,竞争锁
等待方法:
1.痴汉方法,死等,线程进入阻塞态(WAITING)直到有其他线程调用notify方法唤醒
2.等待一段时间,若在该时间内线程被唤醒,则继续执行,若超过相应时间还没有其他线程唤醒此线程,此线程不再等待,恢复执行。
调用wait方法之后:
三、唤醒方法notify()
notify 方法是唤醒等待的线程.
方法notify()也要在同步方法或同步块中调用,该方法是用来通知那些可能等待该对象的对象锁的其它线程,对其发出通知notify,并使它们重新获取该对象的对象锁。
如果有多个线程等待,则有线程调度器随机挑选出一个呈 wait 状态的线程。(并没有 “先来后到”)
在notify()方法后,当前线程不会马上释放该对象锁,要等到执行notify()方法的线程将程序执行完,也就是退出同步代码块之后才会释放对象锁。
注意事项:
notify():随机唤醒一个处在等待状态的线程。
notifyAll():唤醒所有处在等待状态的线程。
无论是wait还是notify方法,都需要搭配synchronized锁来使用(等待和唤醒,也是需要对象)
四、关于wait和notify内部等待问题(重要)
对于wait和notify方法,其实有一个阻塞队列也有一个等待队列。
阻塞队列表示同一时间只有一个线程能获取到锁,其他线程进入阻塞队列
等待队列表示调用wait (首先此线程要获取到锁,进入等待队列,释放锁)
举个栗子:
现有如下定义的等待线程任务
private static class WaitTask implements Runnable {
private Object lock;
public WaitTask(Object lock) {
this.lock = lock;
}
@Override
public void run() {
synchronized (lock) {
System.out.println(Thread.currentThread().getName() + "准备进入等待状态");
// 此线程在等待lock对象的notify方法唤醒
try {
lock.wait();
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(Thread.currentThread().getName() + "等待结束,本线程继续执行");
}
}
}
然后创建三个等待线程:
由于同一时间只有一个线程(随机调度)能获取到synchronized锁,所以会有两个线程没竞争到锁,从而进入了阻塞队列。
这里假如t2先竞争到了锁,所以先会阻塞t1和t3:
又由于调用wait方法会释放锁,调用wait方法的线程t2就会进入等待队列,直到被notify唤醒或者超时自动唤醒。
然后此时lock对象已经被释放了,所以t1和t3 又可以去竞争这个锁了,就从阻塞队列里面竞争锁。
这里假如t3 竞争到了锁,阻塞队列只剩下t1:
然后t3运行到了wait方法,释放锁,然后进入等待队列:
然后重复这些操作~~,最后t1,t2,t3 都进入了等待队列中,等待notify线程唤醒(这里假设notify要放在这些线程start后的好几秒后,因为notify线程也是和这些线程并发执行的,所以等待队列中的线程随时可能被唤醒)
重点来了:
在等待队列中的线程,被notify唤醒之后,会直接回到阻塞队列去竞争锁!!!而不是直接唤醒~
举个栗子:
拿notifyAll()来举例,假如此时等待队列中有三个线程t1,t2,t3,那么调用notifyAll()会直接把它们三个直接从等待队列中进入到阻塞队列中:
然后再去竞争这个锁,去执行wait之后的代码~~
五、完整代码(仅供测试用)
private static class WaitTask implements Runnable {
private Object lock;
public WaitTask(Object lock) {
this.lock = lock;
}
@Override
public void run() {
synchronized (lock) {
System.out.println(Thread.currentThread().getName() + "准备进入等待状态");
// 此线程在等待lock对象的notify方法唤醒
try {
lock.wait();
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(Thread.currentThread().getName() + "等待结束,本线程继续执行");
}
}
}
private static class NotifyTask implements Runnable {
private Object lock;
public NotifyTask(Object lock) {
this.lock = lock;
}
@Override
public void run() {
synchronized (lock) {
System.out.println("准备唤醒");
// 唤醒所有线程(随机)
lock.notifyAll();
System.out.println("唤醒结束");
}
}
}
public static void main(String[] args) throws InterruptedException {
Object lock = new Object();
Object lock2 = new Object();
// 创建三个等待线程
Thread t1 = new Thread(new WaitTask(lock),"t1");
Thread t2 = new Thread(new WaitTask(lock),"t2");
Thread t3 = new Thread(new WaitTask(lock),"t3");
// 创建一个唤醒线程
Thread notify = new Thread(new NotifyTask(lock2),"notify线程");
t1.start();
t2.start();
t3.start();
;
Thread.sleep(100);
notify.start();
// 当前正在执行的线程数
Thread.sleep(2000);
System.out.println(Thread.activeCount() - 1);
}
六、wait和sleep方法的区别(面试题):
wait方法是Object类提供的方法,需要搭配synchronized锁来使用,调用wait方法会释放锁,线程进入WAITING状态,等待被其他线程唤醒或者超时自动唤醒,唤醒之后的线程需要再次竞争synchronized锁才能继续执行。
sleep方法是Thread类提供的方法,调用sleep方法的线程进入TIMED_WAITING状态,不会释放锁,时间到自动唤醒。
来源:https://blog.csdn.net/qq_43575801/article/details/127601039


猜你喜欢
- 前言因为工作原因,需要在项目中集成dubbo,所以去查询dubbo相关文档,发现dubbo目前已经不更新了,所以把目光投向了dubbox,d
- 前言众所周知,Struts2是个非常优秀的开源框架,我们能用Struts2框架进行开发,同时能快速搭建好一个Struts2框架,但我们是否能
- 简介相机模块库,自定义相机,通过简单的调用即可实现拍照、图片裁剪、录像及录像抓拍功能;实现图片压缩,减少图片体积;自定义相机可避免使用系统相
- 因为项目不同,有些公用库而且还是c++的,还有一些带资源的,简单的复制遇到库升级又是一轮配置,编译成aar则解决这些麻烦。但是默认andri
- 前言:数据抽象是一种仅向用户显示基本细节的属性。不向用户显示琐碎或非必需的单元。例如:汽车被视为汽车而不是其单个组件。数据抽象也可以定义为仅
- 一、定义登录控制器目录结构代码:1、创建TUser类package com.demo.pojo;import lombok.AllArgsC
- package tao.cs;import java.io.IOException;import org.ksoap2.SoapEnvelo
- 本文实例讲述了C#检测是否有u盘插入的方法。分享给大家供大家参考。具体如下:该C#代码可监控是否有u盘插入,同时可以监控其它驱动器的变化us
- timer的schedule和scheduleAtFixedRate方法一般情况下是没什么区别的,只在某个情况出现时会有区别--当前任务没有
- java操作Excel数据在 平时 可以使用IO流对Excle进行操作但是现在使用更加方便的第三方组件来实现使用场景1、将用户信息导出为Ex
- 最近在维护项目,app遇到安装在高版本的Android时,以往直接授权和new File(path)的形式不再支持,日志也是说Permiss
- 前言《模式策略的角色扮演游戏》游戏是自制的角色扮演游戏。选择两个角色,然后进行PK,可用来学习JAVA的接口,继承和多态。主要设计1.事先设
- public static String getCharset(File file) { &n
- 设置项这个版本已经取消了defalut settings指定成默认配置的选项,所以配置都是在settings中配置设置项设置统一UTF-8编
- 1、设置ssh安装ssh相关软件包:sudo apt-get install openssh-client openssh-server然后
- 一、Java的前世为什么会产生Java?Java的特点是什么?从C语言开始讲,C语言是一种结构化语言,模块化编程,便于程序的调试,依靠非常全
- 首先在pom文件里引入mqtt的依赖配置<!--mqtt--> <d
- 在c#中可以遍历指定驱动器或指定目录下嵌套目录中的所有文件或者任意深度的文件。通过遍历可以检索string形式的目录名和文件名,也可以检索
- 本文实例讲述了Android布局之LinearLayout自定义高亮背景的方法。分享给大家供大家参考,具体如下:首先创建linearlayo
- 本文实例讲述了C#中lock的用法。分享给大家供大家参考。具体分析如下:lock 关键字可以用来确保代码块完成运行,而不会被其他线程中断。这