“人生海海,起伏常在”
在前面的博客中,我们知道了线程之间是抢占式执行的,因此对于它们的执行顺序是很难预知的,那么这种情况是我们不愿看到的,在实际开发中我们希望合理的协调多个线程之间的执行先后顺序!
如图中的每个NBA球员都是独立的“执行流”,可以认为是一个“线程”,执行着自己相应的任务,在不同情况下有着相应的配合和变化。为了完成得分这个任务,则需要多名运动员相互配合,什么挡拆、换防、卡位等等一系列动作,按照一定的执行顺序执行相应的动作,才能完成得分这个任务。如果运动员之间没有配合,都想拿着球自己打,那么就会乱成一锅粥,完成不了得分这个任务。
在多线程中,完成这个协调任务,主要涉及到三个方法:
- wait() / wait(long timeout) : 让当前线程进入等待状态。
- notify() / notifyAll() : 唤醒在当前对象上等待的线程。
多个线程,需要控制线程之间执行某个逻辑的先后顺序,就可以让后执行的逻辑,使用wait进入等待状态,先执行的线程,完成某些逻辑之后,通过notify唤醒对应的wait。
另外通过notify和wait,也是为了解决“线程饿死”问题:
上图中的场景是这样的,一号滑稽老铁,去ATM取钱,银行那么为了防止泄露密码呢,一般都会弄一个门,一次只能进去一个人,然后把门锁上。一号滑稽老铁去取钱时,发现机器里面没有钱,所以就出去了,然后刚踏出门的时候,又想着再回去看看,于是又把门锁上进行相应操作了,就这样的反反复复的进进出出。虽然此时没有出现“死锁问题”,但是由于后面的滑稽老铁拿不到锁,执行不了任务,此时就出现了“线程饿死”情况!
可能大家在现实生活中遇见这种情况,都会干他,都是这些滑稽老铁是一个个“线程”,是没有能力干他的。线程饿死,是属于概率性问题,和调度器具体的策略有关!
针对上述的问题,同样可以使用 wait / notify来解决:让一号滑稽老铁拿到锁的时候,进行判断当前是否能执行“取钱”的操作,如果能执行则正常执行,如果不能执行,则需要主动释放锁,并且进行“阻塞等待”(通过调用 wait),此时这个线程就不会在后续中,参与锁的竞争了!一旦阻塞到,“取钱”这个条件具备之后,此时再由其他线程通过 通知机制(notify)唤醒这个进程!
wait和notify都是object提供的方法,任意的object对象,都可以使用 wait 和 notify。Java多线程中涉及到的阻塞操作,很多都会抛出上面这个异常,如果另一个线程里调用 Interrupt 方法就能触发这个异常!
声明这个异常之后运行,出现了错误 IllegalMonitorStateException
非法的锁状态异常,意思是在调用当前 wait 的时候,当前锁的状态(加锁状态 / 未加锁状态)是非法的(不正确的)!
wait 中会进行一个操作,就是针对 object 对象,先进行解锁。所以说使用 wait,务必要放到 synchronized 代码块里面,必须得先加上锁,才能谈“解锁”!
由于代码中没有其他线程notify,此时就会一直持续等待下去!
那么通过jconsle可以看到WAITING,没有超时时间的等待。
一号滑稽老铁在进入 ATM 之后,发现里面没钱,就要进行阻塞等待。阻塞等待一定是先释放锁,再进行等待(并且这两个操作,要通过wait来同时进行),如果它“抱着锁等待”,那么此时后面的线程也得不到执行的机会!
如果上述两个操作不是同时执行的,会怎么样呢?
wait 是调用的线程进入阻塞,notify 则是通知 wait 线程被唤醒(另一个线程调用的)。被唤醒的wait,就会重新竞争锁,并且在拿到锁之后继续执行任务。
wait 一共做了三件事:
- 释放锁
- 进入阻塞等待,准备接收通知
- 收到通知后,唤醒,并且重新尝试获取锁
public class Demo2 {public static void main(String[] args) throws InterruptedException {Object locker = new Object();Thread t1 = new Thread(() -> {System.out.println("wait 之前");synchronized (locker) {try {locker.wait();} catch (InterruptedException e) {e.printStackTrace();}}System.out.println("wait 之后");});Thread t2 = new Thread(() -> {Scanner scanner = new Scanner(System.in);scanner.next();synchronized (locker) {locker.notify();}});t1.start();t2.start();}
}
那么上述代码运行时,t1线程会进入等待,等待到t2线程输入完数字之后,再被唤醒,然后继续执行后续的操作!
wait 默认也是“死等”,wait 还提供了带参数的版本,指定超时时间。如果 wait 达到了最大的时间,还没有被 notify 就不会继续等待了,而是直接继续执行。
public class Demo3 {public static void main(String[] args) throws InterruptedException {Object locker = new Object();Thread t1 = new Thread(() -> {System.out.println("wait 之前");synchronized (locker) {try {locker.wait(2000);} catch (InterruptedException e) {e.printStackTrace();}}System.out.println("wait 之后");});Thread t2 = new Thread(() -> {Scanner scanner = new Scanner(System.in);System.out.println("请输入数字:");scanner.next();synchronized (locker) {locker.notify();}});t1.start();t2.start();}
}
从运行结果可以看出,t2线程还没有输入数字,t1线程达到最大时间等待之后,就不在继续等待了,直接自己执行后面的任务了!
wait()和 sleep()的区别:
- wait():是object类的方法。它必须在同步块或同步方法中调用,并且调用它的对象必须被锁定。 sleep():是Thread类的静态方法,可以在任何地方调用,不需要持有锁。
- 调用wait()方法后,当前线程会释放持有的锁,直到被唤醒。 调用sleep()方法之后,线程会暂停执行,但它并不会释放锁。持有的锁仍然保留,其他线程无法获得这些锁。
- 使用wait()的目的是为了被唤醒,sleep()就是固定时间的阻塞,不涉及到唤醒的。(虽然sleep也可以被 Interrupt 唤醒,Interrupt 操作表示的意思不是“唤醒”,而是要终止线程)
唤醒还有一个方法:notifyAll()是一个用来唤醒等待在某个对象监视器上的所有线程的方法。
public class Demo4 {public static void main(String[] args) throws InterruptedException {Object locker = new Object();Thread t1 = new Thread(() -> {System.out.println("t1 wait 之前");synchronized (locker) {try {locker.wait();} catch (InterruptedException e) {e.printStackTrace();}}System.out.println("t1 wait 之后");});Thread t2 = new Thread(() -> {System.out.println(" t2 wait 之前");synchronized (locker) {try {locker.wait();} catch (InterruptedException e) {e.printStackTrace();}}System.out.println("t2 wait 之后");});Thread t3 = new Thread(() -> {System.out.println(" t3 wait 之前");synchronized (locker) {try {locker.wait();} catch (InterruptedException e) {e.printStackTrace();}}System.out.println("t3 wait 之后");});Thread t4 = new Thread(() -> {Scanner scanner = new Scanner(System.in);System.out.println("请输入数字:");scanner.next();synchronized (locker) {locker.notifyAll();}});t1.start();t2.start();t3.start();t4.start();}
}
唤醒所有等待在改对象监视器的线程,所有被唤醒的线程将进入就绪状态。
以上呢便是本期的内容,欢迎收看,我们下一期再见!!!