wait与notify

为什么需要wait方法

  • 一个线程调用sleep方法会陷入休眠状态,虽然释放了cpu但是不会释放锁,这样就会造成浪费,因为陷入sleep状态的线程白白占着共享资源而没有使用它,使得其他线程不得不陷入等待状态。
  • 而wait方法虽然也会使线程陷入暂停状态,与sleep不同的是它会将cpu和锁资源全部释放。
  • 如下 线程1占用锁后休眠了100秒,在线程1休眠期间,线程2无法获得锁,因此无法往下执行,这样白白浪费了cpu资源。
new Thread(() -> {
    synchronized (Code.class){
        System.out.println("t1获得锁");
        try {
            Thread.sleep(100*1000); //休眠时依然占用锁
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}).start();

Thread.sleep(1000);
new Thread(()->{
    System.out.println("t2尝试获得锁");
     synchronized (Code.class){
         System.out.println("t2成功获得锁");
     }
}).start();

此时可以使用wait方法将线程1休眠,这样线程1会释放锁,就不会耽误线程2的执行。
new Thread(() -> {
    synchronized (Code.class){
        System.out.println("t1获得锁");
        try {
            Code.class.wait(100*1000); //休眠时会释放锁
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}).start();

Thread.sleep(1000);
new Thread(()->{
    System.out.println("t2尝试获得锁");
     synchronized (Code.class){
         System.out.println("t2成功获得锁");
     }
}).start();

wait方法的原理

  • wait方法的底层也会使用到monitor对象,如图所示。当一个线程申请到锁之后,就会让monitor对象的owner属性指向自己,但是当它发现虽然自己得到了锁但是因为其他条件不满足,无法执行,就会调用wait方法。
  • 调用wait方法之后的线程会进入waitset队列,线程变为waiting状态。与entrylist队列线程不同的是,entrylist队列线程是申请不到锁资源而陷入等待的状态,而waitset队列线程是申请锁资源后由于其他条件不满足而必须调用wait方法重新释放锁的线程。
  • 陷入waiting状态的线程会在owner线程调用notify或notifyAll时唤醒,唤醒后的线程不会立刻获得锁,而是重新进入entrylist队列重新竞争锁。
  • 因此entrylist可以称为阻塞队列,waitset可以称为条件队列。
    在这里插入图片描述

相关API

  • obj.wait() 让获得obj对象锁的线程到waitset等待。
  • obj.notify() 在obj对象关联的monitor对象上正在waitset等待的线程中挑一个唤醒。
  • obj.notifyALL() 在obj对象关联的monitor对象上正在等待的线程全部唤醒。
  • 他们都属于object对象的方法,必须获得此对象的锁(成为owner线程后),才能调用这几个方法。

wait/notify方法的基本使用

  • wait方法必须在线程获得锁之后才能,调用,如果没有获得锁就调用wait方法会报错,即只有owner线程才有权限唤醒waitset中等待的线程。
public class Demo {
    public static final Object lock = new Object();
    public static final Logger log = LoggerFactory.getLogger(Test.class);
    public static void main(String[] args) throws InterruptedException {
           lock.wait(); //会抛出异常 IllegalMonitorStateException
    }
}
  • 如下代码才能让主线程陷入waiting状态
public class Demo {
    public static final Object lock = new Object();
    public static final Logger log = LoggerFactory.getLogger(Test.class);
    public static void main(String[] args) throws InterruptedException {
        synchronized (lock){
             lock.wait();
        }
    }
}
  • notify方法会在lock对象上面挑一个waiting线程唤醒
public class Demo {
    public static final Object lock = new Object();
    public static final Logger log = LoggerFactory.getLogger(Test.class);
    public static void main(String[] args) throws InterruptedException {
           new Thread(()->{
               log.info("线程0开始执行");
               synchronized (lock){
                   try {
                       lock.wait();
                   } catch (InterruptedException e) {
                       e.printStackTrace();
                   }
               }
               log.info("线程0被唤醒继续执行");
           }).start();

           new Thread(()->{
                log.info("线程1开始执行");
                synchronized (lock){
                    try {
                        lock.wait();
                    } catch (InterruptedException e) {
                       e.printStackTrace();
                }
                }
                log.info("线程1被唤醒继续执行");
           }).start();
           Thread.sleep(2000);
           synchronized (lock){
               log.info("主线程唤醒一个waiting状态的线程");
               lock.notify();
           }
    }
}
//2020-08-26 15:13:48.153 [Thread-0 线程] [INFO] [Test] --- 线程0开始执行
//2020-08-26 15:13:48.153 [Thread-1 线程] [INFO] [Test] --- 线程1开始执行 
//2020-08-26 15:13:50.157 [main 线程] [INFO] [Test] --- 主线程唤醒一个waiting状态的线程 
//2020-08-26 15:13:50.157 [Thread-0 线程] [INFO] [Test] --- 线程0被唤醒继续执行
  • notifyall方法会唤醒lock对象上面所有waiting线程
public class Demo {
    public static final Object lock = new Object();
    public static final Logger log = LoggerFactory.getLogger(Test.class);
    public static void main(String[] args) throws InterruptedException {
           new Thread(()->{
               log.info("线程0开始执行");
               synchronized (lock){
                   try {
                       lock.wait();
                   } catch (InterruptedException e) {
                       e.printStackTrace();
                   }
               }
               log.info("线程0被唤醒继续执行");
           }).start();

           new Thread(()->{
                log.info("线程1开始执行");
                synchronized (lock){
                    try {
                        lock.wait();
                    } catch (InterruptedException e) {
                       e.printStackTrace();
                }
                }
                log.info("线程1被唤醒继续执行");
           }).start();
           Thread.sleep(2000);
           synchronized (lock){
               log.info("主线程唤醒一个waiting状态的线程");
               lock.notifyAll();
           }
    }
}
//2020-08-26 15:17:14.926 [Thread-1 线程] [INFO] [Test] --- 线程1开始执行
//2020-08-26 15:17:14.926 [Thread-0 线程] [INFO] [Test] --- 线程0开始执行
//2020-08-26 15:17:16.925 [main 线程] [INFO] [Test] --- 主线程唤醒一个waiting状态的线程
//2020-08-26 15:17:16.925 [Thread-0 线程] [INFO] [Test] --- 线程0被唤醒继续执行
//2020-08-26 15:17:16.925 [Thread-1 线程] [INFO] [Test] --- 线程1被唤醒继续执行
  • wait方法可以加参数表示有限时的等待,比如wait(3000)会让当前线程等待三秒,如果三秒后还没有其他线程唤醒它,该线程会自动唤醒,如果三秒内它被其他线程唤醒它就不用等三秒唤醒。

一个送烟的例子

  • 小明和其他三个人要干活,他们依次在一个房间干活,该房间只能允许一个人进入。小明烟瘾很大,如果没有烟就无法干活,刚开始小明进入房间,他发现没烟就会休息5秒,而在2秒后有服务员过来送烟,到5秒的时候小明发现有烟了开始干活,干完之后其他人在依次干活。
  • 在这个例子中烟就是小明线程运行的必要条件。
public class Demo {
    public static final Object room = new Object();
    public static volatile boolean flag = false; //标记当前是否有烟
    public static final Logger log = LoggerFactory.getLogger(Test.class);
    public static void main(String[] args) throws InterruptedException {
           new Thread(()->{
               synchronized (room){
                   log.info("有烟没--"+flag);
                   if(flag==false){
                       log.info("没烟 先睡5秒");
                       try {
                           Thread.sleep(5000);
                       } catch (InterruptedException e) { e.printStackTrace(); }
                   }
                   log.info("有烟没--"+flag);
                   if(flag==true){
                       log.info("有烟了 可以开始干活了");
                   }else{
                       log.info("没干成活");
                   }
               }
           },"小明").start();
           for(int i=1;i<4;i++){
               new Thread(()->{
                   synchronized (room){
                       log.info("开始干活了");
                   }
               },"线程"+i).start();
           }

           Thread.sleep(2000);
           new Thread(()->{
               log.info("两秒后开始送烟");
               flag = true;
           },"送烟线程").start();
    }
}
//2020-08-26 16:03:53.567 [小明 线程] [INFO] [Test] --- 有烟没--false
//2020-08-26 16:03:53.570 [小明 线程] [INFO] [Test] --- 没烟 先睡5秒
//2020-08-26 16:03:55.567 [送烟线程 线程] [INFO] [Test] --- 两秒后开始送烟
//2020-08-26 16:03:58.570 [小明 线程] [INFO] [Test] --- 有烟没--true
//2020-08-26 16:03:58.570 [小明 线程] [INFO] [Test] --- 有烟了 可以开始干活了
//2020-08-26 16:03:58.570 [线程3 线程] [INFO] [Test] --- 开始干活了
//2020-08-26 16:03:58.570 [线程2 线程] [INFO] [Test] --- 开始干活了
//2020-08-26 16:03:58.571 [线程1 线程] [INFO] [Test] --- 开始干活了
  • 可以发现刚开始小明占着房间没有干活,直到5秒后才开始干活,那么相当于房间使用权白白浪费了5秒,而且其他人必须要等待小明干完活才能干活,这5秒房间时间使用权完全可以交给没有烟瘾的其他人用。
  • 而且需要注意,送烟线程不能加synchronized ,如果送烟线程加了synchronized ,因为小明已经占了房间,所以送烟线程进不了房间和其他干活的人一样也会阻塞。
  • 使用wait/notify来优化送烟例子
public class Demo {
    public static final Object room = new Object();
    public static boolean flag = false; //标记当前是否有烟
    public static final Logger log = LoggerFactory.getLogger(Test.class);
    public static void main(String[] args) throws InterruptedException {
           new Thread(()->{
               synchronized (room){
                   log.info("有烟没--"+flag);
                   if(flag==false){
                       log.info("没烟 先睡5秒");
                       try {
                           room.wait(5000);
                       } catch (InterruptedException e) { e.printStackTrace(); }
                   }
                   log.info("有烟没--"+flag);
                   if(flag==true){
                       log.info("有烟了 可以开始干活了");
                   }else{
                       log.info("没干成活");
                   }
               }
           },"小明").start();
           for(int i=1;i<4;i++){
               new Thread(()->{
                   synchronized (room){
                       log.info("开始干活了");
                   }
               },"线程"+i).start();
           }

           Thread.sleep(2000);
           new Thread(()->{
               synchronized (room){
                   log.info("两秒后开始送烟 并叫醒小明线程");
                   flag = true;
                   room.notify();
               }
           },"送烟线程").start();
    }
}
//2020-08-26 16:24:06.543 [小明 线程] [INFO] [Test] --- 有烟没--false
//2020-08-26 16:24:06.545 [小明 线程] [INFO] [Test] --- 没烟 先睡5秒
//2020-08-26 16:24:06.545 [线程3 线程] [INFO] [Test] --- 开始干活了
//2020-08-26 16:24:06.545 [线程2 线程] [INFO] [Test] --- 开始干活了
//2020-08-26 16:24:06.545 [线程1 线程] [INFO] [Test] --- 开始干活了
//2020-08-26 16:24:08.544 [送烟线程 线程] [INFO] [Test] --- 两秒后开始送烟 并叫醒小明线程
//2020-08-26 16:24:08.544 [小明 线程] [INFO] [Test] --- 有烟没--true
//2020-08-26 16:24:08.544 [小明 线程] [INFO] [Test] --- 有烟了 可以开始干活了
  • 可以看到小明在发现没有烟后不会占着房间休息,而是让出房间使用权,此时后面其他人依然可以使用房间干活,当送烟线程送到烟后再去休息室将小明叫醒,小明可以继续干活。
  • 但是如果条件队列中阻塞的不止小明一个人,还有小红也在休息,那么送烟线程可能错误唤醒小红而没有把小明唤醒。如下所示,小红没有干成活,小明依然在休息室休息。这种错误唤醒的情况也称为虚假唤醒。
public class Demo {
    public static final Object room = new Object();
    public static boolean flag = false; //标记当前是否有烟
    public static boolean flag2 = false; //标记当前是否有外卖
    public static final Logger log = LoggerFactory.getLogger(Test.class);
    public static void main(String[] args) throws InterruptedException {
        new Thread(()->{
            synchronized (room){
                log.info("外卖到了没--"+flag2);
                if(flag2==false){
                    log.info("没有外卖 进入休息室休息");
                    try {
                        room.wait();
                    } catch (InterruptedException e) { e.printStackTrace(); }
                }
                log.info("外卖到了没--"+flag2);
                if(flag2==true){
                    log.info("有外卖了 可以开始干活了");
                }else{
                    log.info("没干成活");
                }
            }
        },"小红").start();
           new Thread(()->{
               synchronized (room){
                   log.info("有烟没--"+flag);
                   if(flag==false){
                       log.info("没烟 进入休息室休息");
                       try {
                           room.wait();
                       } catch (InterruptedException e) { e.printStackTrace(); }
                   }
                   log.info("有烟没--"+flag);
                   if(flag==true){
                       log.info("有烟了 可以开始干活了");
                   }else{
                       log.info("没干成活");
                   }
               }
           },"小明").start();

           for(int i=1;i<4;i++){
               new Thread(()->{
                   synchronized (room){
                       log.info("开始干活了");
                   }
               },"线程"+i).start();
           }

           Thread.sleep(2000);
           new Thread(()->{
               synchronized (room){
                   log.info("两秒后开始送烟 并叫醒小明线程");
                   flag = true;
                   room.notify();
               }
           },"送烟线程").start();
    }
}
//2020-08-26 16:40:48.319 [小红 线程] [INFO] [Test] --- 外卖到了没--false
//2020-08-26 16:40:48.321 [小红 线程] [INFO] [Test] --- 没有外卖 进入休息室休息
//2020-08-26 16:40:48.321 [线程3 线程] [INFO] [Test] --- 开始干活了
//2020-08-26 16:40:48.322 [线程2 线程] [INFO] [Test] --- 开始干活了
//2020-08-26 16:40:48.322 [线程1 线程] [INFO] [Test] --- 开始干活了
//2020-08-26 16:40:48.322 [小明 线程] [INFO] [Test] --- 有烟没--false
//2020-08-26 16:40:48.322 [小明 线程] [INFO] [Test] --- 没烟 进入休息室休息
//2020-08-26 16:40:50.319 [送烟线程 线程] [INFO] [Test] --- 两秒后开始送烟 并叫醒小明线程
//2020-08-26 16:40:50.319 [小红 线程] [INFO] [Test] --- 外卖到了没--false
//2020-08-26 16:40:50.319 [小红 线程] [INFO] [Test] --- 没干成活
  • 这时候送烟线程就不能使用notify随机唤醒一个线程,而应该使用notifyall将休息室线程全部唤醒,此时小明小红都被唤醒,小明看到有烟了开始干活,小红发现没有外卖不会干活。
new Thread(()->{
    synchronized (room){
        log.info("两秒后开始送烟 并叫醒所有线程");
        flag = true;
        room.notifyAll();
    }
},"送烟线程").start();
  • 但是此时还有一个问题,小红被送烟线程唤醒了却没有干成活,等送外卖线程来了小红已经不在休息室了,小红就永远不会干活了,此时需要进行改进,当小红被叫醒后发现外卖并没有送到,她应该再次进入休息室等待,直到外卖真的送到了。
public class Demo {
    public static final Object room = new Object();
    public static boolean flag = false; //标记当前是否有烟
    public static boolean flag2 = false; //标记当前是否有外卖
    public static final Logger log = LoggerFactory.getLogger(Test.class);
    public static void main(String[] args) throws InterruptedException {
        new Thread(()->{
            synchronized (room){
                while(flag2==false){
                    log.info("外卖到了没--"+flag2);
                    log.info("没有外卖 进入休息室休息");
                    try {
                        room.wait();
                    } catch (InterruptedException e) { e.printStackTrace(); }
                }
                if(flag2==true){
                    log.info("外卖到了 可以干活了");
                }
            }
        },"小红").start();
           new Thread(()->{
               synchronized (room){
                   while(flag==false){
                       log.info("有烟没--"+flag);
                       log.info("没烟 进入休息室休息");
                       try {
                           room.wait();
                       } catch (InterruptedException e) { e.printStackTrace(); }
                   }
                   if(flag==true){
                       log.info("烟到了 可以干活了");
                   }
               }
           },"小明").start();

           for(int i=1;i<4;i++){
               new Thread(()->{
                   synchronized (room){
                       log.info("开始干活了");
                   }
               },"线程"+i).start();
           }

           Thread.sleep(2000);
           new Thread(()->{
               synchronized (room){
                   log.info("两秒后开始送烟 并叫醒所有线程");
                   flag = true;
                   room.notifyAll();
               }
           },"送烟线程").start();
    }
}
//2020-08-26 16:57:57.948 [小红 线程] [INFO] [Test] --- 外卖到了没--false
//2020-08-26 16:57:57.951 [小红 线程] [INFO] [Test] --- 没有外卖 进入休息室休息
//2020-08-26 16:57:57.951 [线程2 线程] [INFO] [Test] --- 开始干活了
//2020-08-26 16:57:57.951 [线程3 线程] [INFO] [Test] --- 开始干活了
//2020-08-26 16:57:57.951 [线程1 线程] [INFO] [Test] --- 开始干活了
//2020-08-26 16:57:57.951 [小明 线程] [INFO] [Test] --- 有烟没--false
//2020-08-26 16:57:57.951 [小明 线程] [INFO] [Test] --- 没烟 进入休息室休息
//2020-08-26 16:57:59.950 [送烟线程 线程] [INFO] [Test] --- 两秒后开始送烟 并叫醒所有线程
//2020-08-26 16:57:59.950 [小明 线程] [INFO] [Test] --- 烟到了 可以干活了
//2020-08-26 16:57:59.950 [小红 线程] [INFO] [Test] --- 外卖到了没--false
//2020-08-26 16:57:59.951 [小红 线程] [INFO] [Test] --- 没有外卖 进入休息室休息
  • 另外可以使用ReentrantLock设置多个条件队列,彻底解决错误唤醒的问题。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值