🏀🏀🏀来都来了,不妨点个关注!
🎧🎧🎧博客主页:欢迎各位大佬!
1. 为什么需要wait()和notify()
在上一篇关于线程安全的问题中,我们提到过线程的调度是无序的,随机的,但很多时候我们都不喜欢随机的东西,很多时候都需要线程能有序的执行,此时就用到了我们今天要介绍的两个方法,wait()和notify()。相信大家都去银行的取款机取过钱,就算没有,也大致知道是个什么样的,进入取款机,然后锁门,然后插卡进行取钱。如下图:
假设此时小万进去取钱,但出现了一个情况:ATM机里没钱,就导致了小万无法取钱,她只好开门出去,但她里面又进去了,想着过了一分钟了,应该有钱了吧,但没有,又出去,然后又进来,如此反复,就会导致其他人无法进去存钱取钱,而小万也无法进行取钱操作。
这放到我们线程之中就是,一个线程t1对一个对象进行加锁后,又解锁,然后立马又进行加锁,然后又解锁,如此反复,其中t1并没有实质性的释放锁,这就会导致t1线程陷入忙等,而其他线程一直获取不到锁,一直处于阻塞等待的状态。这就会导致一个极端的情况:线程饿死。
线程饿死:它指的是一个或多个线程由于某种原因无法获取所需的资源或执行机会,导致它们无法继续正常执行,从而被阻塞在某个状态,不能完成其任务。这种情况通常是由于资源竞争或优先级设置不当导致的。
此时使用wait()和notify()就能很好的解决上述问题,当小万发现ATM机里没钱后就wait,wait就会释放锁,并阻塞等待。而当路人甲乙丙中的一个人获取到锁并进行存钱操作后就会notify唤醒小万,此时问题就得到解决了。
2. wait()方法
wait()方法要做的事就是让某个线程进行等待(阻塞等待),等待另一个线程完成某个任务或者达到某个状态后唤醒该线程。
【注意】 wait()和notify()是Object的方法,所以只要是个类对象,不是内置类型(也叫基本数据类型),都可以使用wait()和notify()方法
wait()具体做的事:
- 让当前线程阻塞等待(将该线程放到等待队列中)
- 释放当前的锁
- 满足一定条件被唤醒,继续尝试获取该锁
wait()结束等待要做的事:
- 其他线程调用该对象的 notify 方法.
- wait 等待时间超时 (wait 方法提供一个带有 timeout 参数的版本, 来指定等待时间).
- 其他线程调用该等待线程的 interrupted 方法, 导致 wait 抛出 InterruptedException 异常.
这就表示满足上述任意条件,调用wait()的线程就会被唤醒。
使用wait()的注意事项:
- 使用wait()需要搭配着synchronized使用,这其实也不难理解,当我们在某个线程中调用wait()方法时,就意味着需要释放该线程的锁了,那如果我们没进行加锁又何来解锁一说。
这里我们可以看看具体没进行synchronized加锁的代码例子:
public class Demo {
public static void main(String[] args) throws InterruptedException {
Object obj = new Object();
System.out.println("wait开始");
obj.wait();
System.out.println("wait结束");
}
}
运行结果:
上面我们提到wait结束等待要做的事是等待其他线程调用notify()方法唤醒该线程,下面我们就介绍下notify()方法。
3. notify()方法
notify()的作用是唤醒等待的线程。
notify()的具体用法:
- notify()是用来通知那些可能等待该对象的对象锁的其它线程,对其发出通知notify,并使它们重新获取该对象的对象锁。
- 如果有多个线程等待,则有线程调度器随机挑选出一个呈 wait 状态的线程。(并没有 “先来后到”)
- 在notify()方法后,当前线程不会马上释放该对象锁,要等到执行notify()方法的线程将程序执行完,也就是退出同步代码块之后才会释放对象锁。
在上面我们的代码中进行了wait()而并没有进行notify()唤醒操作,下面我们就用具体的代码进行举例notify()是如何唤醒的:
public class ThreadDemo17 {
public static void main(String[] args) throws InterruptedException {
Object locker = new Object();
Thread t1 = new Thread( () -> {
try {
System.out.println("wait 开始");
synchronized (locker) {
locker.wait();
}
System.out.println("wait 结束");
} catch (InterruptedException e) {
e.printStackTrace();
}
});
t1.start();
Thread.sleep(1000);
Thread t2 = new Thread( () -> {
synchronized (locker) {
System.out.println("notify 开始");
locker.notify();
System.out.println("notify 结束");
}
});
t2.start();
}
}
这里我们是分别创建了两个线程t1和t2,t1中针对locker对象进行加锁并调用locker的wait()方法,而在t2线程中则针对locker对象进行加锁并调用locker的notify()方法,运行结果如下:
3.1 notifyAll()方法
notify方法只是唤醒某一个等待线程. 使用notifyAll方法可以一次唤醒所有的等待线程.
我们想象下如下场景:此时有多个线程,比如t1、t2,t3都调用了object.wait()方法等待同一个对象。
- 此时在 main 线程中调用 object.notify(),就会随机唤醒上述三个线程中的一个,而另外两个线程仍然是处于 WAITING 状态,
- 但是如果调用 object.notifyAll(),此时就会把上述三个线程全部唤醒,此时这三个线程就会重新竞争锁,再依次执行
理解 notify 和 notifyAll:
- notify 只唤醒等待队列中的一个线程. 其他线程还是乖乖等着
- notifyAll 一下全都唤醒, 需要这些线程重新竞争锁
4. 面试题:请说说 sleep 和 wait 的区别?
- 所属类不同:
sleep()方法:属于Thread类的静态方法
wait()方法:属于Object类的非静态方法 - 锁行为不同:
sleep()方法:当线程执行 sleep 方法时,它不会释放任何锁。也就是说,如果一个线程在持有某个对象的锁时调用了 sleep,它在睡眠期间仍然会持有这个锁。
wait()方法:当线程执行 wait 方法时,它会释放它持有的那个对象的锁,这使得其他线程可以有机会获取该对象的锁。 - 使用条件不同:
sleep()方法:sleep()方法可以在任何地方调用
wait()方法:wait()方法必须在同步方法或同步代码块中调用,这是因为wait()使用的前提是该对象必须进行了加锁,否则就会抛出我们上述说的IllegalMonitorStateException 异常。 - 唤醒方式不同:
sleep()方法:在指定的时间过后,线程会自动唤醒继续执行。
wait()方法:需要依靠 notify()、notifyAll() 方法或者 wait() 方法中指定的等待时间到期来唤醒线程。 - 异常处理不同:
sleep方法:在调用sleep方法时,必须捕获InterruptedException异常,因为线程在等待期间可能会被中断。
wait方法:虽然wait方法也会抛出InterruptedException异常,但在使用wait方法时,通常不需要显式地捕获这个异常,因为它是在同步代码块或同步方法中调用的,这些代码块或方法通常会处理异常。
今天的分享就到此结束了,感谢支持!