等待和通知的标准范式
**一.等待和通知常用的方法:
1)wait() 线程进行等待(阻塞)(Object上的方法)
notify/notifyAll 对线程进行通知(唤醒)
2)notify() 和 notifyAll() 的区别:(简单了解一下)
(简单版本理解)
1.notify(): 表示随机唤醒一个线程(由操作系统内部调度),在这个过程中可能会造成信号丢失的情况(下面通过具体例子说明)
2.notifyAll(): 表示唤醒所有线程,不会造成信号丢失(所以使用的时候建议尽量使用notifyAll)
(复杂版本理解)(可以跳过)
先讲一下两个概念:
-
锁池: 假设线程A已经拥有了某个对象(注意:不是类)的锁,而其它的线程想要调用这个对象的某个synchronized方法(或者synchronized块),由于这些线程在进入对象的synchronized方法之前必须先获得该对象的锁的拥有权,但是该对象的锁目前正被线程A拥有,所以这些线程就进入了该对象的锁池中。
-
等待池:假设一个线程A调用了某个对象的wait()方法,线程A就会释放该对象的锁(因为wait()方法必须出现在synchronized中,这样自然在执行wait()方法之前线程A就已经拥有了该对象的锁),同时线程A就进入到了该对象的等待池中。如果另外的一个线程调用了相同对象的notifyAll()方法,那么处于该对象的等待池中的线程就会全部进入该对象的锁池中,准备争夺锁的拥有权。如果另外的一个线程调用了相同对象的notify()方法,那么仅仅有一个处于该对象的等待池中的线程(随机)会进入该对象的锁池.
然后再来说notify和notifyAll的区别
如果线程调用了对象的 wait()方法,那么线程便会处于该对象的等待池中,等待池中的线程不会去竞争该对象的锁。
当有线程调用了对象的 notifyAll()方法(唤醒所有 wait 线程)或 notify()方法(只随机唤醒一个 wait 线程),被唤醒的的线程便会进入该对象的锁池中,锁池中的线程会去竞争该对象锁。也就是说,调用了notify后只要一个线程会由等待池进入锁池,而notifyAll会将该对象等待池内的所有线程移动到锁池中,等待锁竞争优先级高的线程竞争到对象锁的概率大,假若某线程没有竞争到该对象锁,它还会留在锁池中,唯有线程再次调用 wait()方法,它才会重新回到等待池中。而竞争到对象锁的线程则继续往下执行,直到执行完了 synchronized 代码块,它会释放掉该对象锁,这时锁池中的线程会继续竞争该对象锁。
**
二. 等待和通知的标准范式(重点)
**
1)等待方: a.获取对象的锁
b.循环判断是否满足,不满足用wait()方法
c.条件满足执行业务逻辑
2)通知方: a.获取对象的锁
b.改变条件
c.通知所有等待在对象等待池中的线程
说的云里雾里不如直接用一个简单的业务实例对上面的范式一步一步地进行说明
业务实例: 现在有一个快递的实例Express, 它包含有两个基本属性 km(表示快递的里程数)和site (快递的目的地),我们的业务需求是这个快递在运输的过程中只要它运送的里程数不能超过某一段距离,它就还在路上(表示阻塞); 而快递的目的地也是相同道理,只要它还没有到达某个具体地点,就表示它还在路上(表示阻塞). 我们需要做的就是等到超过某一段距离或到达某一个地点,我们就继续执行自己的业务逻辑(就相当于某个快递到达另一站点后,重新分派到新站点上)
解析:很显然,上面的业务逻辑很符合我们的等待通知范式,快递方在还没到到目的地或者达到某个里程数的时候就会处于等待状态,它等待着通知方把它唤醒。(下面用代码进行说明)
说明:只有当我们的公里数超过100或者目的地是北京时,我们才算完成了一次运输
代码实现:
1.定义一个简单的快递实体类(属性和构造方法)
//初始目的地
public final static String CITY="ShangHai";
//快递的里程数
private int km;
//快递的目的地
private String site;
public Express() {
}
public Express(int km, String site) {
this.km = km;
this.site = site;
}
2.定义两个通知方法的方法
解释:通知方: a.获取对象的锁 (在该方法上加上synchronized关键字,表示获取该对象上的锁)
b.改变条件 (改变km或者site的值表示让等待方达到某个条件,也就是让等待方被唤醒)
c.通知所有等待在对象等待池中的线程 (notifyAll表示通知所有在等待中的线程)
/**
* 变化公里数,然后通知处于wait状态,并需要处理公里
* 数的线程进行业务处理(通知方)
*/
public synchronized void changeKm(){
this.km=101;
notifyAll();
}
/**
* 变化地点,然后通知处于wait状态,并需要处理地点
* 的线程进行业务处理(通知方)
*/
public synchronized void changeSite(){
this.site="BeiJing";
notifyAll();
}
3.定义两个等待方的方法
解释:等待方: a.获取对象的锁 (在该方法上加上synchronized关键字,表示获取该对象上的锁)
b.循环判断是否满足,不满足用wait()方法 (判断km是否大于101或者判断site是否等于北京,不满足的话就还在路上(处于阻塞状态))
c.条件满足执行业务逻辑 (这里只做简单的输出操作)
public synchronized void waitKm(){
while(this.km <= 100){
try {
wait();
System.out.println("check km thread["+
Thread.currentThread().getId()+"] is be notified");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("the km is "+this.km+", I will changed db.");
}
public synchronized void waitSite(){
while(this.site.equals(CITY)){
try {
wait();
System.out.println("check site thread["+
Thread.currentThread().getId()+"] is be notified");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("the km is "+this.site+", I will call user.");
}
完整的快递实体类
package 等待和通知的标准范式;
/**
* @author lenovo
* 快递实体类
*/
public class Express {
//初始目的地
public final static String CITY="ShangHai";
//快递的里程数
private int km;
//快递的目的地
private String site;
public Express() {
}
public Express(int km, String site) {
this.km = km;
this.site = site;
}
/**
* 变化公里数,然后通知处于wait状态,并需要处理公里
* 数的线程进行业务处理(通知方)
*/
public synchronized void changeKm(){
this.km=101;
notifyAll();
}
/**
* 变化地点,然后通知处于wait状态,并需要处理地点
* 的线程进行业务处理(通知方)
*/
public synchronized void changeSite(){
this.site="BeiJing";
notifyAll();
}
public synchronized void waitKm(){
while(this.km <= 100){
try {
wait();
System.out.println("check km thread["+
Thread.currentThread().getId()+"] is be notified");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("the km is "+this.km+", I will changed db.");
}
public synchronized void waitSite(){
while(this.site.equals(CITY)){
try {
wait();
System.out.println("check site thread["+
Thread.currentThread().getId()+"] is be notified");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("the km is "+this.site+", I will call user.");
}
}
下面我们进行简单测试类的书写
1.定义一个里程数变化的线程类
//检查里程数变化的线程,不满足条件线程一直等待
private static class CheckKm extends Thread{
@Override
public void run() {
express.waitKm();
}
}
定义一个地点变化的线程类
//检查地点变化的线程,不满足条件线程一直等待
private static class CheckSite extends Thread{
@Override
public void run() {
express.waitSite();
}
}
测试类的完整代码
解释说明:在main方法中分别启动3个里程数变化的线程,3个地点变化的线程,然后用公里数的通知方进行通知,观察输出结果
package 等待和通知的标准范式;
/**
* @author lenovo
*/
public class TestWN {
private static Express express=new Express(0,Express.CITY);
//检查里程数变化的线程,不满足条件线程一直等待
private static class CheckKm extends Thread{
@Override
public void run() {
express.waitKm();
}
}
//检查地点变化的线程,不满足条件线程一直等待
private static class CheckSite extends Thread{
@Override
public void run() {
express.waitSite();
}
}
public static void main(String[] args) throws InterruptedException {
for(int i=0;i<3;i++){
new CheckKm().start();
}
for(int i=0;i<3;i++){
new CheckSite().start();
}
Thread.sleep(100);
express.changeKm(); //快递公里数发生变化
}
}
运行结果:
- notifyAll()的运行结果
解析:6个线程都得到了通知,但只有3个公里数的线程满足大于100km的条件后继续执行他们的业务逻辑(这也符合我们所说的notifyAll()会通知所有的等待线程,而只有满足自己等待方的判定条件才会继续执行自己的业务逻辑)
2.前面我们说了使用notify()进行通知的话,可能会造成信号丢失(下面进行简单说明)
我们把Express类中所有的notifyAll()方法都改为notify()
运行结果:
解释:上图表明了改变km数的通知方本应该唤醒的是等待km数变化的等待方,可由于操作系统的随机调度,我们的通知信号被改变地点的线程截胡了,而它有无法满足自己超过100km的判断,所以它还是无法执行自己的业务逻辑(所以现在非等待的线程 如Thread[11]虽被唤醒却无法跳出循环,而等待的线程由于无法的到通知信号都在等待状态),这可能就是所谓的旱的旱式涝的涝死,这显然不是我们所希望的,在这里还是建议在开发中尽量使用notifyAll()