目录
wait和notify原理
API
wait 与 sleep的区别
wait 和 notify的正确使用
step1
step2
step3
step4
step5
总结wait+notify
wait和notify原理
- 当我们线程获取某个对象的monitor锁的时候就会成为owner线程,当owner线程条件不满足的时候,就会调用wait方法,该线程就会进入这个monitor里的waitSet里面等待(waitSet里面的线程都是当条件不满足时处于等待状态的线程),线程状态也就从运行状态(RUNNABLE) --> 阻塞状态(WAITING),如果调用的是无参的wait就会一直在WaitSet中等待,而那些没有获取到锁的线程就会进入到monitor的entrylist中进行阻塞等待,线程状态为BLOCKED
虽然处于waitSet和EntryList中的线程状态都是阻塞状态,不会占用CPU时间片.但是两者具有很大的区别
- WaitSet里面的线程是因为条件不满足处于阻塞状态,线程已经获得了锁,但又释放了锁,线程状态为WAITING
- EntryList里面的线程是因为没有竞争到锁而处于阻塞状态,线程状态为BLOCKED
WaitSet中的线程什么时候会被唤醒呢 ?
当owner线程调用notify或者notifyAll方法的时候就会唤醒在WaitSet中等待的线程,唤醒的线程就会进入EntryList中重新竞争锁,而不是立即获取到锁.
EntryList中的线程什么时候被唤醒呢 ?
当Owner线程释放锁的时候就会唤醒EntryList中的线程,在EntryList中的线程就会竞争成为Owner.(竞争锁)
wait的本意就是条件不满足释放锁并且自己进入WaitSet中等待,让其他线程有机会获取锁,
API
- obj.wait() 是让进入object监视器的线程到waitSet中等待
- obj.notify() 是在object上正在waitSet等待的线程随机挑一个来唤醒
- obj.notifyAll() 是让object上正在WaitSet中等待的线程全部唤醒
这些方法都是线程之间进行通信使用的,都属于Object对象的方法. 使用wait notify/notifyAll 必须要获得此对象的锁,才可以调用它们.
演示一下 wait和notify
@Slf4j(topic = "c.demo1")
public class TestWaitNotify {
private static final Object obj = new Object();
public static void main(String[] args) throws InterruptedException {
new Thread(()->{
synchronized (obj) {
log.debug("执行...");
try {
obj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("其他代码");
}
},"t1").start();
new Thread(()->{
synchronized (obj) {
log.debug("执行...");
try {
obj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("其他代码");
}
},"t2").start();
Thread.sleep(1000);
System.out.println("唤醒obj中的线程");
synchronized (obj) {
obj.notify();//唤醒其中一个线程
//obj.notifyAll();//唤醒在waitSet中的所有线程
}
}
}
调用notify
首先线程调度器会给随机一个线程分配CPU时间片,这里线程1先被调度,线程内部 先会获取obj对象的锁->关联上monitor,然后调用obj.wait()方法,也就会释放锁,然后进入monitor的waitSet中等待,接着轮到线程2执行,同样线程2也会先获取到obj对象锁,然后调用wait(),释放锁,进入WaitSet中等待. 1s之后 主线程也获取到obj对象的锁,会执行obj.notify(),就会随机唤醒一个在WaitSet中等待的线程.
调用notifyAll()
对于notifyAll,是会随机唤醒所有在WaitSet中等待的线程.
wait(long timeout)
wait无参数的方法就是使用wait(0)来实现的
wait(long timeout) 是等待一段timeout时长的时间,如果还没有被唤醒,就继续往下执行.
如果被提前唤醒,也是会继续往下执行.(提前执行)
wait 与 sleep的区别
- sleep是Thread类的静态方法,而wait是所有对象都有的方法,是Object的方法
- sleep 不需要强制与 synchronized 配合使用 , 但是 wait 必须和 synchronized 一起使用
- sleep 在睡眠的同时不会释放对象锁, 但是 wait在等待的时候会释放对象锁
//锁对象建议使用final修饰的,因为锁对象不可以变,如果变了,锁的就不是相同的对象了
private static final Object obj = new Object();
public static void main(String[] args) throws InterruptedException {
new Thread(()->{
synchronized (obj) {
log.debug("获得锁");
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
Thread.sleep(1000);
synchronized (obj) {
log.debug("获得锁");
}
}
sleep方法在执行的同时不会释放锁,会一直等待线程睡眠完毕
wait方法
加了wait方法之后,当调用wait方法的时候会释放锁,这样1s后主线程就获得了锁.所以wait方法在等待的时候会释放对象锁
两者共同点就是 : 线程的状态都为TIMEDWAITING
wait() 为 WAITING wait(long timeout) TIMEDWAITING
wait 和 notify的正确使用
一共分为5个步骤进行讲解,每一步都对上一步做的不好的地方做出改进,然后最终改成wait,notify的标准正确使用
场景 : 有一个人小南和5个人要去房间里面干活,但是小南必须要烟才能干活,否则干不了,所以还会有一个线程来给小南送烟.
step1
怎么能让小南,和5个人达到最高的效率干活呢? 不能让他们摸鱼否则工资白给他们了.
@Slf4j(topic = "c.TestWaitNotify1")
public class TestWaitNotify1 {
private static final Object room = new Object();
private static boolean isCigarette = false;
public static void main(String[] args) throws InterruptedException {
//小南等烟
new Thread(()->{
synchronized (room) {
log.debug("是否有香烟 : " +isCigarette);
if(!isCigarette){
log.debug("没有香烟,干不了活,等一会(2s)");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("是否有香烟 : " +isCigarette);
if(isCigarette){
log.debug("小南开始干活");
}
}
},"小南").start();
//还有5个人也要去房间干活
for(int i =0;i<5;++i){
new Thread(()->{
synchronized (room){
log.debug("开始干活");
}
},"其他人" + i).start();
}
Thread.sleep(1000);
//还有一个线程来送烟
new Thread(()->{
synchronized (room){
isCigarette = true;
log.debug("送烟送到了");
}
},"送烟的").start();
}
}
实现了如上代码:
- 有一个小南线程,要进入房间干活(关联monitor->获取锁),但是要想让它干活必须要等烟,等烟到了之后他就会干活,如果烟还没有到,就是用sleep睡眠2s
- 还有5个人也要去同一个room房间干活
- 最后一个是送烟线程,1s之后他会给小南送烟,烟送到了小南就可以干活了.
执行一下看结果 :
缺点 :
- 在小南阻塞的同时其他线程也在阻塞也干不了活.--->可以考虑使用wait,notify(wait可以释放锁)
- 小南没有烟就一直在房间等着,就算送烟的提前把烟给小南送到也是不能立马干活的.(--->可以考虑使用wait,notify(wait可以释放锁))
送烟线程到底加不加synchronized
- 送烟的线程如果加了synchronized(room),就好像小南反锁了门,只要小南一直在房间,别人怎么都进不去.
- 送烟线程如果没有加synchronized(room),就好像送烟的人跳窗户,把烟送来的.
step2
@Slf4j(topic = "c.TestWaitNotify2")
public class TestWaitNotify2 {
private static final Object room = new Object();
private static boolean isCigarette = false;
public static void main(String[] args) throws InterruptedException {
//小南等烟
new Thread(()->{
synchronized (room) {
log.debug("是否有香烟 : " +isCigarette);
if(!isCigarette){
log.debug("没有香烟,干不了活,等一会");
try {
room.wait();
} catch (InterruptedException e) {//加了异常是因为有可能被interrupt打断
e.printStackTrace();
}
}
log.debug("是否有香烟 : " +isCigarette);
if(isCigarette){
log.debug("小南开始干活");
}
}
},"小南").start();
//还有5个人也要去房间干活
for(int i =0;i<5;++i){
new Thread(()->{
synchronized (room){
log.debug("开始干活");
}
},"其他人" + i).start();
}
Thread.sleep(1000);
//还有一个线程来送烟
new Thread(()->{
synchronized (room){
isCigarette = true;
log.debug("送烟送到了");
room.notify();
}
},"送烟的").start();
}
}
如上代码对step1进行了改进,加了wait,notify机制.-->将sleep换成了wait+notify
因为前面也讲了,调用wait方法时候,线程可以释放锁-->这样当小南线程阻塞的时候,其他线程也可以干活了-->其他线程不用阻塞了.
我们看一下执行结果 :
step3
虽然上面step2解决了[小南在阻塞时,其他线程可以正常工作了] 但是如果有多个线程在等待呢.可能又会不一样了.
这时给场景再加一下,在上面的基础上再加一个小女线程也要干活,但是她必须等到外卖在能继续干活.
@Slf4j(topic = "c.TestWaitNotify3")
public class TestWaitNotify3 {
private static final Object room = new Object();
private static boolean isCigarette = false;
private static boolean isTakeOut = false;
public static void main(String[] args) throws InterruptedException {
//小南等烟
new Thread(()->{
synchronized (room) {
log.debug("是否有香烟 : " +isCigarette);
if(!isCigarette){
log.debug("没有香烟,干不了活,等一会");
try {
room.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("是否有香烟 : " +isCigarette);
if(isCigarette){
log.debug("小南开始干活");
}else {
log.debug("小南没有干活");
}
}
},"小南").start();
//小女等外卖
new Thread(()->{
synchronized (room) {
log.debug("是否有外卖 : " +isTakeOut);
if(!isTakeOut){
log.debug("没有外卖,干不了活,等一会");
try {
room.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("是否有外卖 : " +isTakeOut);
if(isTakeOut){
log.debug("小女干活");
}else {
log.debug("小女没有干活");
}
}
},"小女").start();
Thread.sleep(1000);
new Thread(()->{
synchronized (room){
isTakeOut = true;
log.debug("外卖到了");
room.notify();
}
},"送外卖的").start();
}
}
对于上面的代码为了简便代码,就把那5个人去掉了,然后先把送烟的线程去掉换成送外卖的线程,这样更能看出问题,然后加了一个小女线程她要等到外卖才能干活.
我们看一下执行结果:
再加一个等待的线程,就会出现虚假唤醒问题(本来叫醒的是A线程,但是唤醒的却是B线程),原因是notify唤醒的是WaitSet中随机的一个线程.
step4
为了解决虚假唤醒的问题,就在上个代码的基础上加上notifyAll,这样所有线程都会被唤醒.
@Slf4j(topic = "c.TestWaitNotify3")
public class TestWaitNotify3 {
private static final Object room = new Object();
private static boolean isCigarette = false;
private static boolean isTakeOut = false;
public static void main(String[] args) throws InterruptedException {
//小南等烟
new Thread(()->{
synchronized (room) {
log.debug("是否有香烟 : " +isCigarette);
if(!isCigarette){
log.debug("没有香烟,干不了活,等一会");
try {
room.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("是否有香烟 : " +isCigarette);
if(isCigarette){
log.debug("小南开始干活");
}else {
log.debug("小南没有干活");
}
}
},"小南").start();
//小女等外卖
new Thread(()->{
synchronized (room) {
log.debug("是否有外卖 : " +isTakeOut);
if(!isTakeOut){
log.debug("没有外卖,干不了活,等一会");
try {
room.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("是否有外卖 : " +isTakeOut);
if(isTakeOut){
log.debug("小女干活");
}else {
log.debug("小女没有干活");
}
}
},"小女").start();
Thread.sleep(1000);
new Thread(()->{
synchronized (room){
isTakeOut = true;
log.debug("外卖到了");
room.notifyAll();
}
},"送外卖的").start();
Thread.sleep(1000);
new Thread(()->{
synchronized (room){
isCigarette = true;
log.debug("烟到了");
room.notifyAll();
}
},"送烟的").start();
}
}
我又在上面的代码加上个送烟的线程,以便跟好的发现问题
看运行结果
这就发现问题了,小南烟最后送到了,只不过是完一会送到了,但还是没有干上活,让它摸鱼了.
原因就是小南只等待一次,送外卖的线程把小南也叫醒了,但是小南线程只if等待->等待一次,时间过了就不等了,还是没让他干上活. 还是没有彻底解决虚假唤醒问题
step5
notifyAll只解决了某个线程唤醒的问题.
要想真正彻底的解决虚假唤醒问题.我们只要使用while(){wait()},只要条件不成立就让他一直等.就彻底解决了虚假唤醒问题.
@Slf4j(topic = "c.TestWaitNotify4")
public class TestWaitNotify4 {
private static final Object room = new Object();
private static boolean isCigarette = false;
private static boolean isTakeOut = false;
public static void main(String[] args) throws InterruptedException {
//小南等烟
new Thread(()->{
synchronized (room) {
log.debug("是否有香烟 : " +isCigarette);
while(!isCigarette){
log.debug("没有香烟,干不了活,等一会");
try {
room.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("是否有香烟 : " +isCigarette);
if(isCigarette){
log.debug("小南开始干活");
}else {
log.debug("小南没有干活");
}
}
},"小南").start();
//小女等外卖
new Thread(()->{
synchronized (room) {
log.debug("是否有外卖 : " +isTakeOut);
if(!isTakeOut){
log.debug("没有外卖,干不了活,等一会");
try {
room.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("是否有外卖 : " +isTakeOut);
if(isTakeOut){
log.debug("小女干活");
}else {
log.debug("小女没有干活");
}
}
},"小女").start();
Thread.sleep(1000);
new Thread(()->{
synchronized (room){
isTakeOut = true;
log.debug("外卖到了");
room.notifyAll();//解决部分线程虚假唤醒问题
}
}).start();
}
}
这回加了个while(不成立){一直等着} ,就解决了虚假唤醒问题
看执行结果:
总结wait+notify
synchronized(lock){//lock锁对象
while(条件不成立){//条件不成立就一直等着
lock.wait();
}
//符合条件执行业务代码....
}
//这边唤醒的线程要用notifyAll
synchronized(lock){
lock.notifyAll();
}
上面就解决了虚假唤醒问题,以及能够让阻塞的同时,其他线程可以干活,在一定的时间内,可以高效的完成任务.
以后在使用wait+notify的时候,就使用这么一套模板就行.已使用wait+notify就立马想到这套模板.
参考黑马程序员视频
01.001-为什么学习并发_哔哩哔哩_bilibili