前言
我们知道,线程的调度是无序的,但有些情况要求线程的执行是有序的。因此,我们可以使用 wait() 方法来使线程执行有序。
本期讲解 Java 多线程中 synchronized 锁配套使用的 wait 方法、notify方法和notifyAll方法,以及 wait 方法与 sleep 方法之间的区别、为什么要使用 wait 和 notify 方法。
目录
为什么要使用wait()方法和notify()方法?
1. wait()方法
2. notify()方法
3. wait()和notify()方法的使用
4. notifyAll()方法
5. wait()和sleep()的区别
为什么要使用wait()方法和notify()方法?
当我们的 Java 代码使用 synchronized 进行加锁时,会出现线程之间抢占资源的情况。这样就会导致某一个线程不符合条件却反复抢到资源,其他线程参与不了资源。因此得使用 wait() 方法与 notify() 方法来解决该问题。
通过现实生活中的经历举一例子:
把三个线程比做人,把一台 ATM 机比作锁(synchronized)。当这三个线程去取钱时,线程1优先进入了 ATM 机里面取钱。
当 ATM 里面没有钱时,线程1就出了 ATM 机。但由于线程离开了 ATM 机后,会一直与线程2和线程3抢占 ATM 机,因此会造成一个极端的后果,就是线程1一直进入 ATM 机然后出 ATM 机,并且一直循环下去。
以上例子,线程1发现 ATM 没钱可取,却还是反复进出 ATM 这样这样其他线程就无法尝试取钱,对应的就是多线程中的多个线程竞争锁(synchroized)的情况,如何解决以上问题。
使用 wait 方法和 notify 方法。当 ATM(synchronized) 内使用了 wait 方法,线程1取不了钱就会取消锁状态并且处于等待状态,当其他线程进入 ATM 机并且取到了钱这时候就可以使用 notify 方法唤醒 线程1的等待状态,那么线程1又可以进行取钱操作,也就是进行锁的竞争。
在使用 wait 方法后,线程1发现 ATM 里面没有钱可取,就会通过 wait 方法来释放锁并且进行阻塞等待(也就是暂时不参与 CPU 的调度、锁的竞争),这个时候线程2和线程3就能很好的参与取钱这个操作了。
当其他线程 使用 notify 方法时,发现 ATM 里面又有钱可取了。因此就会唤醒线程1的阻塞等待,这时线程1又可以参与 ATM(锁) 的竞争。直到,所有的线程都取到钱为止。
那么使得上述三个线程能供协调的完成取钱这个工作,会用到三个方法:
- wait() 方法/带参数的wait()方法 - 让当前线程进入等待阻塞状态
- notify() 方法 / notifyAll() 方法 - 唤醒当前对象上等待的线程
注意:wait,notify、notifyAll都是 Object 类中的方法。
1. wait()方法
wait 方法使用后:会把当前的执行的线程进行等待阻塞,然后释放当前线程的锁状态,当满足了一定条件后就被唤醒,重新尝试获取这个锁。
wait 结束条件的为:
- 其他线程调用该对象的 notify 方法,
- wait 等待时间超时(wait 方法提供了一个带有参数的版本,可以指定等待时间)
- 其他线程调用该等待的线程的 interrupt 方法,导致 wait 抛出 InterruptedException 异常。
解释:interrupt(),在一个线程中调用另一个线程的interrupt()方法,即会向那个线程发出信号—线程中断状态已被设置。至于那个线程何去何从,由具体的代码实现决定。
wait 和 notify 方法是 Object 类里面的方法,只要是一个类对象都能调用这两个方法。因此,我们可以写出以下代码:
public static void main(String[] args) throws InterruptedException {
Object object = new Object();
System.out.println("Hello object");
object.wait();
System.out.println("object结束");
}
运行后打印:
以上代码运行后打印出一个非法的警告:非法的锁状态异常,因为 wait 方法必须要搭配 synchronized 来使用,脱离了 synchronized 的前提下 使用 wait 就会出现报错。
2. notify()方法
notify 方法是唤醒等待的线程,也就是唤醒调用了 wait 方法的线程。notify 方法作用:
- notify 方法也要在同步方法或同步块中调用,该方法是用来通知那些可能等待该对象的对象锁的
- 其它线程,对其发出通知notify,并使它们重新获取该对象的对象锁
- 如有多个线程处于等待,则线程调度器会随机挑选一个调用 wait 状态的线程。
- 在调用 notify 方法后,当前线程不会立马释放该对象的锁,要等当前调用 notify 方法的线程执行完毕后,才会释放该对象的锁。
在理解 wait 方法和 notify 方法的作用以及使用方法后,下面我们来看下 wait 方法和 notify 方法的结合使用。
3. wait()和notify()方法的使用
代码案例:使用 notify() 方法唤醒 thread1线程。
- 实例化一个 Object 类的对象,调用 wait 和 notify 方法都是用该对象的引用 object 来调用。
- 创建两个线程:线程1和线程2,线程1执行两条语句,线程2也执行两条语句。
- 线程1内使用 object 来调用 wait 方法(两条语句中间调用)
- 线程2内使用 object 来调用 notify 方法(两条语句中间调用)
因此,有以下代码:
public static void main(String[] args) {
Object object = new Object();
Thread thread1 = new Thread(()-> {
synchronized (object) {
System.out.println("thread1开始");
try {
object.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("thread1结束");
}
});
thread1.start();//启动thread1线程
Thread thread2 = new Thread(()-> {
synchronized (object) {
System.out.println("thread2开始");
object.notify();
System.out.println("thread2结束");
}
});
thread2.start();//启动thread2线程
}
运行后打印:
以上代码,输出顺序与需求有所差异,但最终还是达到了效果。
造成输出顺序的不规则原因为:
当 thread1 线程被 wait 前打印了语句“thread1开始”,thread2 线程 中调用了 notify 方法,这时会唤醒 thread1 线程,但是前提得执行完 thread2 中的内容“thread2开始”、“thread2结束”这两个条语句。随后才输出被唤醒的 thread1 线程中的“thread1结束”语句。
当然,既然这样为啥我们不使用 join() 方法呢,thread1 线程完全执行完毕,再执行 thread2线程呢?具体情况具体分析,当我们的代码需求满足使用 join() 方法时,我们就使用 join() 方法。
对应上述代码,join() 方法会使 thread1 线程执行完毕后再执行 thread2 线程。而 wait() 和 notify() 方法会使 thread1 线程执行一部分后,执行 thread2 线程,执行完 thread2 一部分代码后,再执行thread1 线程。这样就满足了特定的条件,类似于上文中线程取钱情况。大家可以自行尝试一番。
注意,wait() 方法的初心就是为了等待、阻塞的效果。在 synchronized 内调用 wait() 方法,得按 Alt+Enter 这两个组合键来 try/catch 异常。
4. notifyAll()方法
notifyAll() 方法是用来唤醒当前对象的所有调用 wait() 的线程。案例:
- 有三个线程,线程1为thread1、线程2为thread2、线程3为thread3
- thread1 中输出两条语句“thread1开始”、“thread1结束”
- thread2 中输出两条语句“thread2开始”、“thread2结束”
- thread1 和 threa2 在两条语句中间通过 Object 类的引用调用 wait() 方法造成阻塞
- thread3 线程通过 Object 类的引用调用 notifyAll() 方法唤醒所有的阻塞
因此,前两个线程都通过 Object 类的引用调用了 wait() 方法造成阻塞,最后一个线程调用 notifyAll() 则唤醒了所有调用 wait() 方法的线程,如以下代码:
public static void main(String[] args) {
Object object = new Object();//实例化一个Object类的对象
Thread thread1 = new Thread(()->{
synchronized (object) {
System.out.println("thread1-开始");
try {
object.wait();//thread1中调用wait方法
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("thread1-结束");
}
});//创建thread1线程
thread1.start();//启动thread1线程
Thread thread2 = new Thread(()->{
synchronized(object) {
System.out.println("thread2-开始");
try {
object.wait();//thread2调用wait方法
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("thread2-结束");
}
});//创建thread2线程
thread2.start();//启动thread2线程
Thread thread3 = new Thread(()->{
synchronized (object) {
object.notifyAll();//thread3中调用notifyAll方法
System.out.println("thread3调用了notifyAll方法");
}
});//创建thread3线程
thread3.start();//启动thread3线程
}
运行后打印:
以上代码,通过 notifyAll() 方法唤醒了所有等待的线程。如果我把 notifyAll() 方法替换为 notify() 方法,此时就会随机唤醒一个正在等待的线程。如以下打印结果:
通过上面截图,我们可以观察到随机唤醒的是 thread1 线程。
5. wait()和sleep()的区别
wait 与 sleep 之间的区别:
- wait() 方法是 Object 类底下的方法,sleep() 方法是 Thread 类底下的静态方法。
- wait()方法是搭配 synchronized 来使用的,而 sleep() 则不需要。
- 核心区别,初心不同,wait() 方法是为了避免线程之前的抢占资源(解决线程之间的顺序控制),而 sleep() 方法是为了让线程休眠特定的时间。
- wait() 方法有一个带参数的写法是用来体现超时的提醒(避免死等),因此用起来就感觉和 sleep() 方法一样。
案例:
有两线程,main 线程与 thread 线程,main 线程内包含 thread 线程,main 线程内有“Hello main”语句, thread 线程内有“Hello thread”语句。
在 main 线程内创建一个 thread 线程,并且在 thread 线程内使用 Object 类对象调用带参的 wait() 方法,并设置参数 为2000。
在main 方法内使用 Object 类对象调用 notify() 唤醒 thread 线程。使得输出 main 线程内语句后停顿两秒输出 thread 线程内语句。
有以下代码:
public static void main(String[] args) {
Object object = new Object();//实例化一个Object类对象
Thread thread = new Thread(()->{
synchronized (object) {
try {
object.wait(2000);//thread调用了带参wait方法,停顿了两秒
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Hello thread");
}
});//创建thread线程
thread.start();//启动thread线程
synchronized (object) {
object.notify();//main方法内调用notify方法
}
System.out.println("Hello main");
}
运行后打印:
输出“Hello main”语句后停顿了两秒,输出“Hello thread”线程。
重点:
- wait、notify、notifyAll都是 Object 类的方法
- wait、notify、notifyAll 必须搭配 synchronized 关键字来使用
- 不带参数的 wait 方法会造成死等、带参数的 wait 方法则不会
- wait 方法的初心就是为了线程处于等待、阻塞状态
- notify 方法的初心就是为了唤醒同一对象调用 wait 方法的随机一个线程
- notifyAll 方法的初心就是为了唤醒同一对象调用 wait 方法的所有线程
🧑💻作者:一只爱打拳的程序猿,Java领域新星创作者,阿里云社区优质创造者。
🗃️文章收录于:Java多线程编程
🗂️JavaSE的学习:JavaSE
🗂️Java数据结构:数据结构与算法
本期博文到这里就结束了,感谢点赞、评论、收藏、关注~