美文网首页
notify()与wait()

notify()与wait()

作者: LENN123 | 来源:发表于2020-04-05 17:17 被阅读0次
    前言

    如果每个线程能够独自完成自己的任务,就最好不过了。但是现实是很多情况下各个线程之间需要沟通和协作,通过相互配合的方式共同完成一个任务。Java中的Object类中提供了如下方法,让我们做到能够在完成某些操作时通知(唤醒)一个或多个进程,或者当判定不满足一些条件时,主动释放掉获得的锁,并将自己加入该锁的等待队里。

    public final native void wait(long timeout) throws InterruptedException; 
    public final native void notifyAll();  // 唤醒所有线程
    public final native void notify();     // 唤醒一个线程
    
    为什么需要notify()与wait()

    结合具体场景分析这两个方法的作用,首先我们假设有一个厨房,厨房外有一把锁,每次只允许一位食客进来吃饭。厨师在厨房里做好饭后,就离开厨房等多个食客进入就餐。下面给出Kitchen类的具体实现:

    • Kitchen类
    public class Kitchen {
        // 厨房的门锁
        private Object lock;
        // 食物准备好了吗?
        private volatile boolean isOk;
    
        public Kitchen(){
            this.lock = new Object();
            this.isOk = false;
        }
        // 现在食物准备好了吗?
        public boolean isOk() { return isOk;}
       // 厨师将该标志设为true,表示师傅已经准备好
        public void setOk(boolean ok) { isOk = ok; }
        // 该厨房大门上的锁
        public Object getLock() { return lock; }
    }
    
    

    我们设立了一个isOk的标志, 当其为true时,表示厨师已经把食物准备好了,食客看到这个标志为true,就明白现在可以就餐了。
    我们再编写表示厨师做饭行为的Cook类,以及表示食客就餐行为的Eat类,两个类都实现了Runnable接口的run()方法,可交给线程执行。

    • Cook类
    public class Cook implements Runnable {
    
        private Kitchen kitchen;
        public Cook(Kitchen kitchen) {
            this.kitchen = kitchen;
        }
        @Override
        public void run() {
            synchronized (kitchen.getLock()) {
                System.out.println("厨师进入厨房准备开始烹饪食物");
                // 开始做饭
                try {
                    Thread.sleep(1000L);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                // 饭做好了,设置isOk变量为true
                kitchen.setOk(true);
                System.out.println("食物已经做好了,大家可以吃了");
                // 离开同步块,释放厨房的锁
            }
        }
    }
    

    Cook类的逻辑很简单,代表厨师的线程会尝试获得厨房上的锁,当成功获得锁进入厨房后,就开始做饭,中间的sleep方法模拟这个做饭所消耗的时间。当厨师做好饭以后,就把厨房的isOk标志设置为true,表示饭做好了,大家可以吃了,然后释放厨房上的锁,离开厨房。

    • Eat类
    public class Eat implements Runnable {
        private Kitchen kitchen;
        private String name;
        public Eat(Kitchen kitchen, String name) {
            this.kitchen = kitchen;
            this.name = name;
        }
        @Override
        public void run() {
            // 拿到厨房的锁,进入厨房
            synchronized (kitchen.getLock()) {
                System.out.println(name + "进入了厨房,准备开始吃饭");
                // 判断食物做好了吗?没有的话原地等待
                while (!kitchen.isOk()) { }
                // 食物做好了, 开始吃饭
                System.out.println(name + "吃完了饭, 离开了厨房");
            }
        }
    }
    

    当代表食客的线程拿到了厨房的锁,成功进入厨房后,他首先要判断,饭做好了吗?如果没做好,这个食客就开始不停判断饭做没做好,因为isOk这个条件变量是用volatile关键字修饰的,保证了可见性,那么只要有厨师把饭做好,那么当前食客线程肯定能感知到,然后从while循环跳出。但其实这里很明显会发生思索,拿到了厨房锁的食客线程一直自旋等待食物做好,厨师却无法获得锁进去烹饪食物,两者开始了无休止的相互等待。这里我们先放置不管,马上再来解决这个问题。我们先来模拟一种不会发生死锁的理想的场景。

    • 一个理想的场景
      厨师先进厨房做饭,做好后多个食客进入就餐


      理想场景

    给出这个场景下具体的java实现

    public class notifyDemo {
        public static void main(String[] args) {
            Kitchen kitchen = new Kitchen();
    
            Thread cooker = new Thread(new Cook(kitchen));
            Thread tom = new Thread(new Eat(kitchen,"tom"));
            Thread ben = new Thread(new Eat(kitchen,"ben"));
    
            cooker.start();
            // 主线程休眠,让厨师线程先把饭做好
            try {
                Thread.sleep(10000L);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
    
            tom.start();
            ben.start();
        }
    }
    

    cooker线程先执行,然后主线程休眠等待cooker线程把饭做好之后启动tom和ben两个食客线程进去用餐,运行结果如下一切正常。

    厨师进入厨房准备开始烹饪食物
    食物已经做好了,大家可以吃了
    tom进入了厨房,准备开始吃饭
    tom吃完了饭, 离开了厨房
    ben进入了厨房,准备开始吃饭
    ben吃完了饭, 离开了厨房
    
    Process finished with exit code 0
    

    现在让我们颠倒一下顺序,让食客tom线程先运行。

    public class notifyDemo {
       public static void main(String[] args) {
           Kitchen kitchen = new Kitchen();
    
           Thread cooker = new Thread(new Cook(kitchen));
           Thread tom = new Thread(new Eat(kitchen,"tom"));
           Thread ben = new Thread(new Eat(kitchen,"ben"));
           // 先让食客进入,此时发生死锁
           tom.start();
           // 主线程休眠,让厨师线程先把饭做好
           try {
               Thread.sleep(10000L);
           } catch (InterruptedException e) {
               throw new RuntimeException(e);
           }
    
           cooker.start();
           ben.start();
       }
    }
    

    和我们预期的一致,发生了死锁。

    tom进入了厨房,准备开始吃饭
    //发生死锁,开始无限等待
    Process finished with exit code 130 (interrupted by signal 2: SIGINT)
    

    解决这个死锁问题的关键是要在食客进入厨房后,如果发现厨师没有把饭做好,就主动释放厨房的锁,并主动让出cpu供别的线程使用,过一段时间再试,而不是原地空转。

     while (!kitchen.isOk()) {
          释放锁,让出cpu,等待饭做好
     }
    

    怎么实现呢?让出cpu似乎很简单,我们只要让食客线程sleep一段时间就好了,让我们试一下。

     while (!kitchen.isOk()) {
        try {
            Thread.sleep(1000L);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
      }
    
    public class notifyDemo {
        public static void main(String[] args) {
            Kitchen kitchen = new Kitchen();
    
            Thread cooker = new Thread(new Cook(kitchen));
            Thread tom = new Thread(new Eat(kitchen,"tom"));
            Thread ben = new Thread(new Eat(kitchen,"ben"));
            // 先让食客进入,此时发生死锁
            tom.start();
            // 主线程休眠,让厨师线程先把饭做好
            try {
                Thread.sleep(500L);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
    
            cooker.start();
            //ben.start();
        }
    }
    

    tom拿到锁进入厨房后,发现食物还没做好,于是sleep让出cpu,随后cooker线程启动开始做饭。观察一下运行结果。

    tom进入了厨房,准备开始吃饭
    // 依然发生死锁,因为tom线程sleep的时候没有释放锁
    Process finished with exit code 130 (interrupted by signal 2: SIGINT)
    

    最后和之前一样发生了死锁,原因在于当tom这个线程sleep的时候,他没有释放掉厨房的锁,后来启动的cooker线程,依然无法拿到锁进去做饭。
    为了解决这个问题,就需要借助notify()和wait()方法了。

    1. 首先要明确这两个方法都是和锁对象进行绑定的,比如上文的Object lock = new Object()。使用的时候是在lock这个锁对象上调用wait()notify()方法。
    2. 当我们在一个锁对象上调用wait()的方法时,该线程会被加入和这个锁关联的等待队列中,同时时候放掉锁,让出cpu
    3. 当在一个锁对象上调用notify()方法时,会随机通知等待队列中的一个线程,“我这边准备好了,你可以准备开始争用我要释放的锁了”。
    4. 执行了notify()方法的线程继续执行,直到退出同步代码块,释放占用的锁。被通知的线程抢占到锁后继续从之前调用lock.wait()方法的下一行语句开始继续执行。

    整体的运行流程如下:

    执行流程

    按照这个逻辑修改代码

    • Eat类里添加等待逻辑
    public void run() {
            // 拿到厨房的锁,进入厨房
            synchronized (kitchen.getLock()) {
                System.out.println(name + "进入了厨房,准备开始吃饭");
                // 判断食物做好了吗?没有的话原地等待
                while (!kitchen.isOk()) {
                    try {
                        // 饭还没好,把自己加入这个锁的等待队列中去
                        kitchen.getLock().wait();
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
                // 食物做好了, 通知等待的食客准备开始吃饭
                System.out.println(name + "吃完了饭, 离开了厨房");
            }
        }
    
    • Cook类里添加通知逻辑
    public void run() {
            synchronized (kitchen.getLock()) {
                System.out.println("厨师进入厨房准备开始烹饪食物");
                // 开始做饭
                try {
                    Thread.sleep(5000L);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                // 饭做好了,设置isOk变量为true
                kitchen.setOk(true);
                // 通知一个线程,饭已经做好了。
                kitchen.getLock().notify();
                System.out.println("食物已经做好了,大家可以吃了");
                // 离开同步块,释放厨房的锁
            }
        }
    
    • 测试结果,利用线程间通知机制,死锁问题被解决了。
    tom进入了厨房,准备开始吃饭
    厨师进入厨房准备开始烹饪食物
    食物已经做好了,大家可以吃了
    tom吃完了饭, 离开了厨房
    
    Process finished with exit code 0
    
    不要在循环之外调用wait方法!

    现在我们来关注Eat实现里的一个细节

    while (!kitchen.isOk()) {
        try {
            // 饭还没好,把自己加入这个锁的等待队列中去
            kitchen.getLock().wait();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }
    

    当这个线程从wait()方法中醒来,说明此时厨师已经把饭做好了,isOk肯定是等于true的,可是在这里却在while循环里再重新判断了一次isOk的值,这是不是多此一举呢?现在我们考虑这样一种情况,我们厨师线程不采用notify()的方法,而采用notifyAll()方法,通知多个食客进程准备用餐,同时,这回厨师只做了一份食物,因此当一个食客吃完后就把isOk标志位设为false,表示饭吃完了。

    补充:notifyAll()方法与notify()的区别是,notify()随机通知一个线程,而notifyAll()通知所有等待的线程。

    我们修改Eat类和Cook类的执行逻辑,来模拟上述过程。

    • Eat类
    public void run() {
            // 拿到厨房的锁,进入厨房
            synchronized (kitchen.getLock()) {
                System.out.println(name + "进入了厨房,准备开始吃饭");
                // 判断食物做好了吗?没有的话原地等待
                if (!kitchen.isOk()) {
                    try {
                        // 饭还没好,把自己加入这个锁的等待队列中去
                        kitchen.getLock().wait();
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
                // 模拟吃饭所花的时间
                try {
                    Thread.sleep(1000L);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                // 把饭吃完,设置标志位为false
                kitchen.setOk(false);
                System.out.println(name + "吃完了饭, 离开了厨房");
            }
        }
    
    • Cook类
      public void run() {
            synchronized (kitchen.getLock()) {
                System.out.println("厨师进入厨房准备开始烹饪食物");
                // 开始做饭
                try {
                    Thread.sleep(5000L);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                // 饭做好了,设置isOk变量为true
                kitchen.setOk(true);
                // 通知一个线程,饭已经做好了, 通知所有线程。
                kitchen.getLock().notifyAll();
                System.out.println("食物已经做好了,大家可以吃了");
                // 离开同步块,释放厨房的锁
            }
        }
    
    • Test类
     public static void main(String[] args) {
            Kitchen kitchen = new Kitchen();
    
            Thread cooker = new Thread(new Cook(kitchen));
            Thread tom = new Thread(new Eat(kitchen,"tom"));
            Thread ben = new Thread(new Eat(kitchen,"ben"));
            // 先让食客进入,此时发生死锁
    
            tom.start();
    
            try {
                Thread.sleep(1000L);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            cooker.start();
            ben.start();
        }
    
    • 实验结果
    tom进入了厨房,准备开始吃饭
    ben进入了厨房,准备开始吃饭
    厨师进入厨房准备开始烹饪食物
    食物已经做好了,大家可以吃了
    ben吃完了饭, 离开了厨房
    tom吃完了饭, 离开了厨房
    
    Process finished with exit code 0
    

    可以发现,即使食物只有一份,tomben却都吃上了饭,这显然是不可能的。问题的关键在于我们没有把wait()放在循环里。我们之前提到过,位于某个锁等待队列的线程,被通知并抢占到对应锁后,从wait()语句的下一行开始执行,我们看看如果用if语句会发生什么。

    不正确的执行流程

    根据上述的执行流程我们发现,同时被通知到的食客线程Ben把isOk设置为false,而Tom线程却不再去判断isOk是否为true,吃到了不存在的饭。
    wait()中恢复的线程,其当初的条件变量很可能又被其他线程修改过,需要重新判断,因此需要被放在while循环里

    条件判断和notify()/wait()方法要在一个同步代码块内部!

    其实就是要求我们把条件判断和执行notify()/wait()方法,合并成一个原子方法。否则很可能会发生饥饿现象,考虑如下的执行流程。

    发生饥饿

    Tom线程发现isOk=false,准备执行wait()的方法等待厨师把饭做完,但是由于两者并非原子操作,在执行wait()方法之前,厨师线程把饭做好,并设置条件变量为isOk=true,可惜Tom线程将永远无法发现饭做好了,一直到饿死。

    相关文章

      网友评论

          本文标题:notify()与wait()

          本文链接:https://www.haomeiwen.com/subject/ndwjphtx.html