前言
由于线程之间是抢占式执⾏的,因此线程之间执⾏的先后顺序难以预知,但是实际开发中有时候我们希望合理的协调多个线程之间的执⾏先后顺序。比如,一个线程修改了一个对象的值,而另一个线程感知到了变化,然后进行相应的操作,整个过程开始于一个线程,而最终执行又是另外一个线程。而让多个线程相互协调的办法,我们可以使用 wait、notify 来完成。
本篇文章会先解释 wait、notify 的基础概念,以及 wait、notify 为什么在 synchronized 中 和 wait 与 sleep 的区别 的相关面试题。接着,我们将实现一个简单的应用场景,并在这个场景中处理并发问题 -- 模拟ATM取钱。
前期回顾:Java 并发编程】理解同步锁、可重入锁与死锁问题
代码地址:模拟ATM取钱
目录
wait ⽅法
wait 的概念
如果一个线程在等待某个条件发生变化,而改变这个条件超出了当前方法的控制能力。通常,这个条件将由另外一个任务来改变。此时继续执行这个方法就已经没有了意义,因为条件没有改变结果还不符合预期。你肯定是不想再让程序进行这种无意义的空循环,这也称为忙等待,通常是一种不良CPU的周期使用方式。此时你可以使用 wait 方法在等待外部变化的时候将这个程序挂起,当这个条件改变了,再使用 notify 或者 notifyall 将这个任务唤醒,并去执行条件改变后的代码。
这里简单罗列一下 wait 需要做的事情
使当前执⾏代码的线程进⾏等待 |
释放当前的锁 |
满⾜⼀定条件时被唤醒,重新尝试获取这个锁 |
wait 结束等待的条件
其他线程调⽤该对象的 notify 或者 notifyall 方法,将 wait 唤醒 |
wait 超时等待 (wait ⽅法提供⼀个带有参数的版本,来指定等待时间) |
其他线程调⽤该等待线程的 interrupted ⽅法,导致 wait 抛出 InterruptedException 异常 |
wait 的使用
class WaitTest extends Thread{
Queue<Integer> queue = new LinkedList<>();
public synchronized void addTask(Integer val) {
this.queue.add(val);
}
public synchronized Integer getTask() {
while (queue.isEmpty()) {
try {
this.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
return queue.remove();
}
@Override
public void run() {
for (int i = 0; i < 10; i++) {
addTask(i);
}
for (int i = 0; i < 10; i++) {
System.out.print(getTask()+" ");
}
getTask();
}
}
class Test1{
public static void main(String[] args) {
Thread t1 = new WaitTest();
t1.start();
}
}
打印结果
0 1 2 3 4 5 6 7 8 9 (程序被挂起)
以上代码重点集中在 getTask() ,如果当前队列不为空,就获取队头元素;如果为空,wait 就会把当前线程挂起,并将锁释放,直到另一个线程拿到这把锁触发新增条件,并使用 notify 才能把 wait 唤醒结束循环。试想,如果 while 语句没有 wait ,将会如何?这里有两个弊端:
(1) while 循环永远不会退出。因为线程在执行 while 循环时,已经 getTask() 入口获取了 this 锁,其他线程根本无法调用 addTesk(),因为 addTesk() 执行条件也是获取 this 锁。 |
(2) 由于 while 是空语句,继续执行没有任何意义,就是上述所说的忙等待,占用 CPU 大量资源。 |
wait ⽅法还提供⼀个带有参数的版本,来指定等待时间。如果超过了等待就会自动退出等待。
this.wait(3000);
运行结果:
0 1 2 3 4 5 6 7 8 9 退出 wait 退出 wait 退出 wait 退出 wait ...
由于程序虽然解除了对 wait 的等待,但是队列为空的条件并没有改变,这个线程依旧在执行 while 循环。
这样在执⾏到 wait() 之后就⼀直等待下去,那么程序肯定不能⼀直这么等待下去了。这个时候就需要使⽤到了另外⼀个⽅法唤醒的⽅法 notify()
notify 方法
notify 的概念
notify ⽅法是唤醒等待的线程
notify ⽅法是⽤来通知那些可能等待该对象的对象锁的其它线程,对其发出通知 notify,并使它们重新获取该对象的对象锁。 |
如果有多个线程等待,则有线程调度器随机挑选出⼀个呈 wait 状态的线程。 |
在 notify ⽅法后,当前线程不会⻢上释放该对象锁,要等到执⾏ notify ⽅法的线程将程序执⾏完,也就是退出同步代码块之后才会释放对象锁。 |
notify 的使用
class WaitTest {
Queue<Integer> queue = new LinkedList<>();
public synchronized void addTask(Integer val) {
this.queue.add(val);
this.notify();
}
public synchronized Integer getTask() {
while (queue.isEmpty()) {
try {
this.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
return queue.remove();
}
}
class Test1{
public static void main(String[] args) {
WaitTest wt = new WaitTest();
Thread t1 = new Thread(()->{
for(int i=0;i<10;i++){
wt.addTask(i);
}
for(int i=0;i<10;i++){
System.out.print(wt.getTask()+" ");
}
wt.getTask();
});
Thread t2 = new Thread(()->{
wt.addTask(1000);
wt.addTask(2000);
wt.getTask();
});
t1.start();
t2.start();
}
}
运行结果:
0 2 3 4 5 6 7 8 9 1000
进程已结束,退出代码为 0
来分析一下以上代码:我们在 addTask 方法中添加了 notify 唤醒机制,当一个线程因为队列为空被 wait 挂起时,当前 this 锁会释放。我们就可以通过另外一下线程运行 addTask 方法拿到这把锁并且唤醒 wait ,告诉它队列不为空了赶紧起来。这个时候 wait 重新获得锁,并退出 while 循序。
notifyAll ⽅法
notify ⽅法只是唤醒某⼀个等待线程,使⽤ notifyAll ⽅法可以⼀次唤醒所有的等待线程
class WaitTask implements Runnable {
private Object locker;
public WaitTask(Object locker) {
this.locker = locker;
}
@Override
public void run() {
synchronized (locker) {
while (true) {
try {
System.out.println(Thread.currentThread().getName()+"wait 开始");
locker.wait();
System.out.println(Thread.currentThread().getName()+"wait 结束");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
class NotifyTask implements Runnable {
private Object locker;
public NotifyTask(Object locker) {
this.locker = locker;
}
@Override
public void run() {
synchronized (locker) {
System.out.println("notify 开始");
locker.notify();
System.out.println("notify 结束");
}
}
}
class Test1{
public static void main(String[] args) throws InterruptedException {
Object locker = new Object();
Thread t1 = new Thread(new WaitTask(locker));
Thread t3 = new Thread(new WaitTask(locker));
Thread t4 = new Thread(new WaitTask(locker));
Thread t2 = new Thread(new NotifyTask(locker));
t1.start();
t3.start();
t4.start();
Thread.sleep(1000);
t2.start();
}
}
运行结果:
Thread-0wait 开始
Thread-2wait 开始
Thread-1wait 开始
notify 开始
notify 结束
Thread-0wait 结束
Thread-0wait 开始
当我们把 notify 换成 notifyAll 看看会发生什么
@Override
public void run() {
synchronized (locker) {
System.out.println("notifyAll 开始");
locker.notifyAll();
System.out.println("notifyAll 结束");
}
}
Thread-0wait 开始
Thread-2wait 开始
Thread-1wait 开始
notifyAll 开始
notifyAll 结束
Thread-0wait 结束
Thread-0wait 开始
Thread-1wait 结束
Thread-1wait 开始
Thread-2wait 结束
Thread-2wait 开始
我们发现我们所有被 wait 阻塞的线程都被唤醒了,注意:虽然是同时唤醒被阻塞的线程, 但是这些线程需要竞争锁。 所以并不是同时执⾏,⽽仍然是有先有后的执⾏。
wait 与 notify
wait 与 notify 的关系就是上述这种
面试一问:
wait 和 notify 为什么要在 synchronized 代码块中
先来看一下,如果 wait、notify 不在 synchronized 代码块中会发生什么呢?
我们发现程序会抛出异常
Exception in thread "Thread-1" Exception in thread "Thread-0" java.lang.IllegalMonitorStateException: current thread is not owner
at java.base/java.lang.Object.notify(Native Method)
at WaitTest.addTask(wait练习.java:9)
at Test1.lambda$main$1(wait练习.java:39)
at java.base/java.lang.Thread.run(Thread.java:1570)
java.lang.IllegalMonitorStateException: current thread is not owner
at java.base/java.lang.Object.notify(Native Method)
at WaitTest.addTask(wait练习.java:9)
at Test1.lambda$main$0(wait练习.java:30)
at java.base/java.lang.Thread.run(Thread.java:1570)
IllegalMonitorStateException 报错的原因:
IllegalMonitorStateException 是在调用 object 的 wait 和 notify 或 notifyAll 方法的时候可能会出现的异常。 在调用上述三个方法的时候,线程必须获得该对象的对象级别锁,换句话说,出现这个异常的原因是因为,调用 wait 和notify 或 notifyAll 的对象没有在同步方法(synchronized修饰的方法)或者同步代码块中。
首先是我们理解的基础
wait() 和 notify()是用来实现多个线程之间的一个协调;wait() 表示让线程进入到阻塞状态,notify() 表示让阻塞的线程被唤醒, notifyAll() 则表述唤醒所有被阻塞的线程。 wait() 和 notify() 必然是成对出现的。 如果一个线程被wait()方法阻塞, 那么必然要另一个线程通过notify() 唤醒,从而实现多个线程之间的通信。
使用 synchronized 的原因:
在多线程里面要实现多个线程之间的通信,除了管道以外,只能通过共享变量的方式来实现,也就是说第一个线程修改了公共共享变量,别的线程获得修改后的变量的值,从而完成数据的一个通讯;但是多线程的本身是具有并行执行的一个特性,也就是说,在同一个时间,多个线程是可以同时执行的,那么这种情况下,其它现在在访问共享变量之前,必须要知道,第一个线程已经修改了这个共享变量,否则就需要等待,或者是拿到原始数据,基于此基础上运输,产生数据不一致的场景;同时第一个线程在数据修改后,还需要把那个已经处于等待状态下的其它线程唤醒,所以在这种场景下,需要去实现线程之间的通信就必须要有一个静态条件,去控制多线程什么时候条件等待什么时候条件唤醒。
而 synchronized 关键字就可以实现这样一个互斥条件,也就是在通过共享变量来实现多个线程通信的一个场景里面,参与通信的线程必须竞争到这共享变量的一个锁资源,才能够有资格对共享变量进行修改,那么修改完之后释放锁,其他线程就可以再次竞争同一个共享的锁,来获取修改之后的数据,从而完成线程之间的一个通信;所以这就是为什么wait()、notify() 必须需要在 synchronized 代码块中使用。
wait 与 sleep
相同点:都可以使线程阻塞等待,但是等待的场景是不一样的
不同点:
声明的位置不同,sleep() 声明在 Thread 类,wait() 声明在 Object 类 |
关于是否可以指定睡眠时间,sleep 函数必须指定,wait 可以指定也可以不指定 |
sleep() 会让当前正在运行的、占用CPU时间片的线程挂起指定时间,休眠时间到自动苏醒进入可运行状态;切记,是不会由睡眠状态直接变为运行状态的。wait() 方法用来线程间通信,如果设置了时间,就等待指定时间;如果不设置,则该对象在其它线程被调用 notify()、notifyAll() 方法后进入可运行状态,才有机会竞争获取对象锁。 |
适用场景不同,sleep()可以在任何需要的场景下调用,wait()必须在同步代码块中或者同步方法中的监视器中。 |
关于是否释放同步监视器,如果两方法都是使用在同步代码块或同步方法中,sleep() 不会释放锁,wait() 会释放锁,并进入线程等待池。 |
sleep()线程控制自身流程。wait()用来线程间通信,使拥有该对象锁的线程等待直到指定时间或notify()。 |
模拟ATM取钱
场景模拟:
(1) 首先我们最普通的场景是这样的:ATM 相当于一个小房间,仅支持一个人取钱。如果 ATM 中有人,其他用户必须等待。
(2) ATM 机的钱并不是无限的,它是银行通过运钞车运送并存在这里的固定金额。但是当有用户取的钱超出了当前 ATM 的最大值,就不能取钱成功。当然这个用户也不能即不能取钱也不让别人取,一直堵在 ATM 里不出来。
(3) 当 ATM 不够用户取钱时,要及时补充当前 ATM 的资金。
代码模拟:
import java.util.Random;
import java.util.Scanner;
public class Bank extends Thread{
private static final int MAX_MONEY = 10000;
public static int Money = MAX_MONEY;
private static volatile boolean flag = false;
public static volatile boolean flag1 = false;
// 用户取钱操作
public synchronized static void drawMoney(int money){
Money -= money;
System.out.println(Thread.currentThread().getName()+" 取走了:"+money+" 当前银行还剩下:"+Money);
}
private synchronized void saveMoney(){
Scanner sc = new Scanner(System.in);
System.out.println("请稍入运钞车放入银行的金额");
int money = sc.nextInt();
System.out.println("运钞车给银行补充了" + money + "元");
Money += money;
flag = false;
}
@Override
public void run() {
Thread currentThread = Thread.currentThread();
while(!currentThread.isInterrupted()){
if(flag){
saveMoney();
}
}
}
public void setFlag(boolean flag) {
this.flag = flag;
}
}
class cashCarrier extends Thread{
private Bank bank;
@Override
public void run() {
Thread currentThread = Thread.currentThread();
while(!currentThread.isInterrupted()) {
if(bank.flag1){
synchronized (cashCarrier.class) {
cashCarrier.class.notify();
}
bank.flag1 = false;
}
}
}
}
class Person extends Thread{
Bank bank;
public Person(Bank bank) {
this.bank = bank;
}
@Override
public void run() {
// 银行当前资金满足用户提取
Thread currentThread = Thread.currentThread();
while(!currentThread.isInterrupted()) {
Random random = new Random();
int myMoney = random.nextInt(1000);
synchronized (Person.class) {
if (bank.Money <= myMoney) {
synchronized (cashCarrier.class) {
try {
bank.setFlag(true);
System.out.println("ATM取款机钱不够"+Thread.currentThread().getName()+"等待中...");
bank.flag1 = true;
Thread.sleep(3000);
cashCarrier.class.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}else{
bank.drawMoney(myMoney);
}
}
}
}
}
class Test{
public static void main(String[] args) throws InterruptedException {
Bank bank = new Bank();
cashCarrier cash = new cashCarrier();
Person[] person = new Person[3];
for(int i=0;i<person.length;i++){
person[i] = new Person(bank);
person[i].setName(i+"线程");
}
for(int i=0;i<person.length;i++){
person[i].start();
}
bank.start();
cash.start();
}
}
运行结果:
...
1线程 取走了:303 当前银行还剩下:2159
2线程 取走了:616 当前银行还剩下:1543
2线程 取走了:342 当前银行还剩下:1201
2线程 取走了:430 当前银行还剩下:771
ATM取款机钱不够0线程等待中...
请稍入运钞车放入银行的金额
2线程 取走了:205 当前银行还剩下:566
2线程 取走了:12 当前银行还剩下:554
ATM取款机钱不够2线程等待中...
1线程 取走了:385 当前银行还剩下:169
1线程 取走了:4 当前银行还剩下:165
ATM取款机钱不够1线程等待中...
ATM取款机钱不够0线程等待中...
10000
运钞车给银行补充了10000元
1线程 取走了:225 当前银行还剩下:9940
1线程 取走了:76 当前银行还剩下:9864
2线程 取走了:450 当前银行还剩下:9414
1线程 取走了:697 当前银行还剩下:8717
2线程 取走了:740 当前银行还剩下:7977
...
小结
wait 和 notify 主要用于线程通信,多个线程相互协调。
wait 、notify 的使用必须加锁且加的是同一把锁,否则会抛出 IllegalMonitorStateException 异常。 |
wait ,notify ,notifyAll 都不属于 Thread 类,而是属于 Object 基础类。 |
在 while 循环里而不是 if 语句下使用 wait,这样会在线程暂停恢复后都检查 wait 的条件,并在条件实际上并未改变的情况下处理唤醒通知。 |
notify 方法只会通知等待队列中的第一个相关线程,所有线程都有机会,因为线程抢占式不确定的。notifyAll 通知所有等待该竞争资源的线程。 |