【线程基础】wait/notify/sleep/join等重要用法以及注意事项
使用方法
wait#
方法 | 作用 |
---|---|
wait() | 将当前运行的线程挂起(即让其进入阻塞状态),直到notify或notifyAll方法来唤醒线程。 |
wait(long timeout) | 该方法与wait()方法类似,唯一的区别就是在指定时间内,如果没有notify或notifAll方法的唤醒,也会自动唤醒。 |
wait(long timeout,long nanos) | 相对于wait(long timeout)来说,拥有更精确的控制调度时间。 |
notify#
方法 | 作用 |
---|---|
notify() | 唤醒一个被wait挂起的线程 |
notifyAll() | 唤醒所有被wait挂起的线程 |
sleep#
方法 | 作用 |
---|---|
sleep(long timeout) | 让当前线程暂停指定的时间(毫秒) |
yield#
方法 | 作用 |
---|---|
yield() | 暂停当前线程,以便其他线程有机会执行。将线程的Running状态转变为Runnable状态 |
join#
方法 | 作用 |
---|---|
join() | 让父线程等待子线程执行完成后再执行 |
join(long millis) | content2 |
join(long millis, int nanos) | 等待millis 毫秒终止线程,假如这段时间内该线程还没执行完,将取消等待。 |
join(long millis, int nanos) | 相对于join(long millis)来说,拥有更精确的控制调度时间。 |
注意事项
wait 必须在 synchronized 保护的同步代码中使用#
首先我们先看看wait方法源码里面是怎么写的
wait method should always be used in a loop:
synchronized (obj) {
while (condition does not hold)
obj.wait();
... // Perform action appropriate to condition
}
This method should only be called by a thread that is the owner of this object's monitor.
这里的意思是说,在使用 wait 方法时,必须把 wait 方法写在 synchronized 保护的 while 代码块中,并始终判断执行条件是否满足,如果满足就往下继续执行,如果不满足就执行 wait 方法,而在执行 wait 方法之前,必须先持有对象的 monitor 锁,也就是通常所说的 synchronized 锁。
我可以先看一下下面这段代码
public class WaitsMain {
private static String a = "";
public static void main(String[] args) {
Thread thread01 = new Thread(()->{
System.out.println(Thread.currentThread().getName()+" 抢到了锁");
try {
System.out.println(Thread.currentThread().getName()+" 释放锁");
a.wait();
System.out.println(Thread.currentThread().getName()+" 执行结束了");
} catch (InterruptedException e) {
e.printStackTrace();
}
});
Thread thread02 = new Thread(()->{
System.out.println(Thread.currentThread().getName()+" 抢到了锁");
a.notify();
System.out.println(Thread.currentThread().getName()+" 唤醒了锁");
});
thread01.start();
thread02.start();
}
}
在不加锁的情况下,这段代码如果执行,会出现如下情况
Thread-0 抢到了锁
Thread-0 释放锁
Thread-1 抢到了锁
Exception in thread "Thread-0" Exception in thread "Thread-1" java.lang.IllegalMonitorStateException
at java.lang.Object.notify(Native Method)
at src.com.thread.waits.WaitsMain.lambda$main$1(WaitsMain.java:22)
at java.lang.Thread.run(Thread.java:748)
java.lang.IllegalMonitorStateException
at java.lang.Object.wait(Native Method)
at java.lang.Object.wait(Object.java:502)
at src.com.thread.waits.WaitsMain.lambda$main$0(WaitsMain.java:12)
at java.lang.Thread.run(Thread.java:748)
可以看到报错信息出现了IllegalMonitorStateException这个报错,这个报错会在三种情况下抛出:
- 当前线程不含有当前对象的锁资源的时候,调用obj.wait()方法;
- 当前线程不含有当前对象的锁资源的时候,调用obj.notify()方法。
- 当前线程不含有当前对象的锁资源的时候,调用obj.notifyAll()方法。
很明显上段代码中就是因为Thread-1线程没有获取到当前对象锁资源然后去调用notify方法导致出现异常,所以如果在不加锁的情况下,贸然的去使用wait和notify可能会导致整个流程的操作没有在一个原子性的环境下去完成。接下来我们去改进一下上面这段代码
public class WaitsMain {
private static String a = "";
public static void main(String[] args) {
Thread thread01 = new Thread(()->{
synchronized (a){
System.out.println(Thread.currentThread().getName()+" 抢到了锁");
try {
System.out.println(Thread.currentThread().getName()+" 释放锁");
a.wait();
System.out.println(Thread.currentThread().getName()+" 执行结束了");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
Thread thread02 = new Thread(()->{
synchronized (a){
System.out.println(Thread.currentThread().getName()+" 抢到了锁");
a.notify();
System.out.println(Thread.currentThread().getName()+" 唤醒了锁");
}
});
thread01.start();
thread02.start();
}
}
控制台输出:
Thread-0 抢到了锁
Thread-0 释放锁
Thread-1 抢到了锁
Thread-1 唤醒了锁
Thread-0 执行结束了
在我们改进后的代码之后,会发现加了锁的代码,wait方法在释放monitor锁的时候,就必须要先进入到synchronized内持有这把锁,这样才能够提高线程的安全性。
wait/notify 和 sleep 方法的异同#
相同点#
- 他们都可以让线程阻塞
- 它们都可以响应 interrupt 中断:在等待的过程中如果收到中断信号,都可以进行响应,并抛出 InterruptedException 异常。
不同点#
- wait 方法必须在 synchronized 保护的代码中使用,而 sleep 方法并没有这个要求。
- 在同步代码中执行 sleep 方法时,并不会释放 monitor 锁,但执行 wait 方法时会主动释放 monitor 锁。
- sleep 方法中会要求必须定义一个时间,时间到期后会主动恢复,而对于没有参数的 wait 方法而言,意味着永久等待,直到被中断或被唤醒才能恢复,它并不会主动恢复。
- wait/notify 是 Object 类的方法,而 sleep 是 Thread 类的方法。
为什么 wait/notify/notifyAll 被定义在 Object 类中,而 sleep 定义在 Thread 类中#
- 因为 Java 中每个对象都有一把称之为 monitor 监视器的锁,由于每个对象都可以上锁,这就要求在对象头中有一个用来保存锁信息的位置。这个锁是对象级别的,而非线程级别的,wait/notify/notifyAll 也都是锁级别的操作,它们的锁属于对象,所以把它们定义在 Object 类中是最合适,因为 Object 类是所有对象的父类。
- 因为如果把 wait/notify/notifyAll 方法定义在 Thread 类中,会带来很大的局限性,比如一个线程可能持有多把锁,以便实现相互配合的复杂逻辑,假设此时 wait 方法定义在 Thread 类中,如何实现让一个线程持有多把锁呢?又如何明确线程等待的是哪把锁呢?既然我们是让当前线程去等待某个对象的锁,自然应该通过操作对象来实现,而不是操作线程。
yield()注意事项#
- yield是一个静态的本地方法(native)
- 调用yield后,yield告诉当前线程把运行机会交给线程池中有相同优先级的线程。
- yield不能保证,当前线程迅速从运行状态切换到就绪状态。
- yield只能是将当前线程从运行状态转换到就绪状态,而不能是等待或者阻塞状态。
join()的实现原理#
join()方法实现是通过wait()。 当main线程调用t.join时候,main线程会获得线程对象t的锁(wait 意味着拿到该对象的锁),调用该对象的wait(),直到该对象唤醒main线程 ,比如退出后。这就意味着main 线程调用t.join时,必须能够拿到线程t对象的锁。
详细可以看一下join的源码
join()和yield的区别#
yield()做的是让当前运行线程回到可运行状态,以允许具有相同优先级的其他线程获得运行机会。因此,使用yield()的目的是让相同优先级的线程之间能适当的轮转执行。但是,实际中无法保证yield()达到让步目的,因为让步的线程还有可能被线程调度程序再次选中。
join()方法则是可以使得一个线程在另一个线程结束后再执行。如果join()方法在一个线程实例上调用,当前运行着的线程将阻塞直到这个线程实例完成了执行。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· 【自荐】一款简洁、开源的在线白板工具 Drawnix