wait(等待) / notify (通知)
线程在操作系统上的调度是随机的~
那么我们想要控制线程之间执行某个逻辑的先后顺序,那该咋办呢?
可以让后执行的逻辑,使用wait, 先执行的线程,在完成某些逻辑之后,通过notify来唤醒对应的wait.
另外,通过wait notify 也是为了解决"线程饥饿"问题.
举个例子:
有一个ATM机,滑稽1,进入ATM机,锁上房门后,结果发现ATM里面没钱了~
没钱了咋办,那就出来呗,于是这个老哥,就开锁出来了,但是这个老哥前脚刚迈出这个门半步,心里想:“我要不再进去看看”,于是这个老哥又把门锁上就又进去了,结果还是没钱,就出来了,走到门口,就又进去了.
就这样进进出出.他后面的滑稽也进不去~
把这些滑稽想象成一个个线程.第一个线程进进出出,后面的线程进不去,这就是"线程饥饿".
更准确的描述: 线程饥饿(Thread Starvation)是指在多线程程序中,某个线程无法获得所需的资源或执行所需的操作,导致其长时间等待或无法正常工作的情况。
针对上述问题,我们可以使用 wait / notify 来解决
让1号滑稽,拿到锁的时候进行判断
判断当前是否能执行"取钱"操作,如果能执行,就正常执行.
如果不能执行,那就主动释放锁,并且"阻塞等待"(通过调用wait),此时这个线程就不会在后续参与锁的竞争了.
一直阻塞到,"取钱"的条件具备了.
此时,再由其他线程通过机制(notify) 唤醒这个线程.
接着刚才的例子:
滑稽1,进入ATM之后,发现没钱,就要阻塞等待(wait)~
阻塞等待,一定是先释放锁,再等待,如果他抱着锁等待,别人就不能使用ATM机了.
wait 既然要释放锁,那么前提就是必须先加上锁.
public static void main(String[] args) throws InterruptedException {
Object obj = new Object();
System.out.println("wait 之前");
// 使用 wait 必须先加上锁!!
synchronized(obj) {
obj.wait();
}
System.out.println("wait 之后");
}
如果不先加锁,就会报异常:
加锁后:
由于代码中没有notify,所以wait将一直等待下去~
wait要进行解锁,也要进行阻塞等待.(阻塞就是为了收到通知)
解锁和进行阻塞等待是同时执行的(打包成原子的,wait内部已经做好了)
如果不是原子的,不是同时执行,会咋样呢?
假设wait是分成两步:
- 释放锁
- 执行等待
那么这两者之间,就可能发生线程切换.
比如,此处运钱的线程穿插进来了,执行了放钱操作,并且会通知所有的等待他的线程来取钱,但是由于1号滑稽还没有执行等待操作,所以上述运钱的通知不会被1号滑稽感知到~
此时,当1号滑稽执行等待的时候,由于错过了通知,那么他将持续等待下去,无法被及时唤醒了.
wait 使调用的线程进入阻塞.
notify 则是通知wait的线程被唤醒.被唤醒的wait,就会重新竞争锁,并且在拿到锁之后,再继续执行.
在使用 wait 或 notify 时,必须要确保先加锁,才能执行.
private static Object locker = new Object();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(()->{
synchronized(locker) {
System.out.println("t1 wait 之前");
try {
// 使用 wait 必须先加上锁!!
locker.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("t1 wait 之后");
}
});
Thread t2 = new Thread(()->{
synchronized(locker) {
System.out.println("t2 notify 之前");
Scanner scanner = new Scanner(System.in);
scanner.next(); // 这里主要是通过这个next来构造"阻塞"
// 使用 notify 必须先加上锁!!
locker.notify();
System.out.println("t2 notify 之后");
}
});
t1.start();
t2.start();
}
wait默认是死等, () 内不写东西就是默认.
wait还提供带参数的版本,来指定超时时间,如果wait达到了最大的超时时间,还没有notify,那么就不会继续等待了,而是直接继续执行.
这样看来,wait和sleep看起来就有点相似了~
但是,wait和sleep是有本质区别的:
- 使用wait的目的是为了提前唤醒,而sleep就是固定时间的阻塞,不涉及到唤醒.虽然sleep可以被Interrupt唤醒,但是Interrupt操作表示的意思不是"唤醒",而是要终止线程了.
- wait必须要搭配synchronized使用,并且wait会先释放锁,同时进行等待
- sleep和锁无关,如果不加锁,sleep也能正常使用,如果加了锁,不会释放锁,而是"抱着锁"一起睡.其他线程是无法拿到锁的~
notify唤醒wait的线程,假如有多个线程都在同一个对象wait上,此时notify是如何唤醒的呢?
答: 随机唤醒其中的一个线程~
和notify相对的,还有一个操作, notifyAll 唤醒所有等待的线程.不过很少使用就是了.
因为一个一个唤醒(多执行几次notify)整个程序的执行过程是比较有序的,如果一下唤醒所有,这些被唤醒的线程就会无序的竞争锁.
总结
使用 wait(等待) / notify (通知) 可以控制线程之间执行某个逻辑的先后顺序,也可以解决"线程饥饿"问题.
线程饥饿(Thread Starvation)是指在多线程程序中,某个线程无法获得所需的资源或执行所需的操作,导致其长时间等待或无法正常工作的情况。
wait 一共做了三件事:
- 释放锁.
- 进入阻塞等待,准备接收通知.
- 收到通知以后,唤醒,并且重新尝试获取锁.
wait / notify 都是Object提供的方法.
wait / notify 都需要搭配synchronized使用,即先加锁,再使用.
wait / notify 要想通知生效,还需要确保是同一个对象.
wait提供了无参数版本,也提供了带参数版本(超时时间)
如果有多个线程都在wait, notify会随机唤醒其中的一个线程.
notifyAll 能够唤醒所有.
notify 调用次数超过wait的次数(没有人等待,也notify了),没有副作用~