wait和notify分别对应着等待和通知机制
它们和join的用途类似,使用wait是基于线程是随机调度的这一底层
我们这里引入wait和notify,是为了能够从应用层面上,干预到多个不同线程代码的执行顺序.
这里说的干预,不是影响系统的线程调整策略(内核仍然是无序随机调度)
wait和notify
join,我们这样做,相当于是在应用程序代码中,让后执行的线程主动放弃被调度的机会,就可以让先执行的线程,先把对应的代码执行完了
例子:饥饿线程
举一个ATM机的例子,假如现在有ABCD4个人,每个人都对应一个线程,A现在要取钱,BCD现在要存钱.现在A进入到ATM机里面,进行取钱操作,可是他发现取钱时,ATM里面没有钱,那么这个时候A发现没钱,就要出来,但是A没取到钱,所以A又要进去取钱,一直都是A在操作ATM,其他人操作不了ATM,也就是A一直在执行线程调度,其他线程进入锁状态,但是当A脱离锁之后,发现A的任务还没有完成,又接着抢夺锁,可能多次都是他抢到锁了,这样其他线程就无法执行,我们称这种现象为饥饿线程.
针对这种情况,我们使用wait,notify,当ATM里面没钱的时候,我们让A使用WAIT,进入WAITING状态,在wait方法调用处,会进入WAITING状态此时A线程会自动释放锁,并且放弃参与到锁竞争当中去,那么此时其他线程往ATM里面去存钱,当存到一定数额时,调用notify方法,此时,A线程就会就会被唤醒,又参与到锁的竞争中去.
伪代码:
while(true){
synchronize()
{if(ATM有钱)
取钱操作;
break;
else{
wait();//如果没钱,就主动进入阻塞状态
}
}
wait的作用:
1.释放锁
2.进入阻塞等待
3.当其他线程调用notify的时候,wait解除阻塞,并重新获取到锁
join与wait的区别:
join是等待另一个线程执行完之后,才继续执行
wait是等待notify通知;
Object locker1=new Object();
locker1.wait();
直接使用wait方法会进入报错,我们要在对应的锁里面使用才可以.
调用wait方法要让线程解锁,就必须和synchronized的锁对象object是一致的,
synchronized(object){
object.wait();}
并且wait在唤醒之后,依然要重新获取到原来的object锁
其他线程在调用notify时候,也同样需要相同的锁对象.
Object locker1=new Object();
synchronized (locker1){
System.out.println("wait之前");
locker1.wait();
System.out.println("wait之后");
可以观测到,改线程在调用wait方法之后,进入到阻塞WAITING状态
try {
locker1.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
wait方法和join,sleep一样,也是有可能会被interrupt提前唤醒的
Object object=new Object();
Thread t1=new Thread(()->{
synchronized(object){
System.out.println("wait之前");
try {
object.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("wait之后");
}
});
Thread t2=new Thread(()->{
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
synchronized (object){
System.out.println("notify之前");
object.notify();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("notify之后");
}
});
t1.start();
t2.start();
执行结果,可以看到,在notify之后,t2释放锁之后,t1立马获取到锁,在之前wait调用处,接着往下执行.
上述代码执行逻辑:
1)t1执行之后,就会立即先拿到锁,并且在wait之前进行打印,之后进入wait方法,就会释放锁,并且进入阻塞状态.
2)t2执行起来后,会先执行sleep(1000),目的是确保t1能够先拿到锁.
3)t2sleep结束之后,因为t1是WIATING状态,不会进入锁竞争,因此没有线程和t2争夺锁,那么t2就能顺利拿到锁,进行打印,执行notify操作,此时t1被唤醒,但是此时是由WAITING状态进入到BLOCKED猪状态,会一直尝试获取锁
4)由于此时t2还没有释放锁,所以t1并不能立刻马上拿到锁,此时的阻塞是由于锁竞争引起的.得等到t2执行完释放锁之后,t1才能拿到锁,
5)t2执行完notify,释放锁,t2执行完毕,t1的wait就可以获取到锁了,继续执行打印
wait两个方法:
1)不带參数的wait,如果没有人进行通知,他就会一直等,俗称死等
2)带參数的,会设置一个等的最长时间.如果超过这个时间还没有notify进行通知,那么他就不会再等了
在实际的开发当中,使用wait带參数的情况为多,在商业程序上,我们要考虑"鲁棒性"_容错能力,就是"你对他越粗鲁,他表现的越棒",即使出现一些差错,也不会有太大影响,能够自动恢复.
2)wait和notify彼此之间,是通过object对象联系起来的
object1.wait()
object2.wait()
此时是无法唤醒的,必须是两个对象一致才能唤醒.,此外,一次只能唤醒一个等待的线程
3)notifyAll
唤醒这个对象上的所有等待线程
假设有很多个线程,都使用同一个对象wait
针对这个对象进行notifyAll,此时就会全都唤醒~~
但是,要注意,这些线程在wait返回的时候,要重新获取锁,就会因为锁竞争,使这些线程实际上是一个一个串行执行的(谁先拿到锁,后拿到,也是不确定的),
相比之下,notify更好控制,notifyAll全都唤醒之后,不太好控制
wait和sleep之间的区别:
wait提供了一个带有超时时间的版本
sleep也是能指定时间~~
都是时间到,就继续执行,解除阻塞了.
wait和sleep都可以被提前唤醒
wait通过notify进行唤醒
sleep通过interrupt唤醒
使用wait的主要目标,一定是不知道等多少时间的前提下使用的,所谓的超时时间是兜底的.
使用sleep,一定是知道需要等待多长时间的前提下使用的,虽然能够提前唤醒,但是通过异常唤醒,这个操作不应该作为正常的业务流程(通过异常唤醒,说明程序是出现了一些特殊的情况了);
sleep提前唤醒,是通过异常的方式,正常的业务流程不应该依赖于异常处理,异常处理认为是一些不就措施.