技术学习笔记
Toggle Dark/Light/Auto mode Toggle Dark/Light/Auto mode Toggle Dark/Light/Auto mode

3.1等待与通知机制

while 轮询存在缺陷

  • A 线程执行每一步的时间较长。

        public class ThreadA extends Thread{
            private MyList list;
    
            public ThreadA(MyList list){
                super();
                this.list = list;
            }
            @Override
            public void run(){
                super.run();
                try{
                    for (int i = 0; i < 10; i++) {
                        list.add();
                        System.out.println(Thread.currentThread().getName() + " Add " + i);
                        Thread.sleep(1000);
                    }
                }catch (InterruptedException e){
                    e.printStackTrace();
                }
    
            }
        }
    
    
  • B线程执行轮询的时间较短

        public class ThreadB extends Thread{
            private MyList list;
            public ThreadB(MyList list){
                super();
                this.list = list;
            }
            @Override
            public void run(){
                super.run();
                try{
                    while (true){
                        if(list.size() >= 5){
                            System.out.println("B Up to five.");
                            throw new InterruptedException();
                        }
                        Thread.sleep(20);
                    }
                }catch (InterruptedException e){
                    e.printStackTrace();
                }
    
            }
        }
    

while轮询

线程 A 和 执行循环每轮会休息 1s,线程 B 每隔 20ms 对 size 进行访问。而线程 B 的轮询,若时间太长则无法准确捕捉 list.size的变化;若时间太短则耗费大量的 CPU 资源。

所以,使用 while 来轮询变量或者对象属性,会造成 CPU 资源的浪费;轮询间隔越短,越耗费资源。

我们使用一种称为 wait/notify 的机制来解决这个问题。

多线程共享变量,会有同步的问题,线程无法确保读取到的变量值是不是最新版本,而 wait/notify机制 恰好可以解决同步这个问题。

wait/notify 机制

Java 的对象都有 wait() 方法和 notify() 方法,但需要同步加锁才能使用这个 wait/notify 机制。

  • 两个线程共用一个对象监视器 lock 『共用一把锁』

    一个线程中,调用对象监视器的 wait(),该线程释放锁,并且进入等待状态。

    另外一个线程,在需要的时候调用对象监视器的 notify() 方法,会释放锁,并且随机唤醒等待同一共享资源的一个线程,但 notify 是等到同步块执行完成才释放锁

    • ThreadA.java

      public class ThreadA extends Thread{
          private Object lock;
      
          public ThreadA(Object lock){
              super();
              this.lock = lock;
          }
          @Override
          public void run(){
              super.run();
              synchronized (lock){
                  System.out.println("start.");
                  try {
                      lock.wait();
                  } catch (InterruptedException e) {
                      e.printStackTrace();
                  }
                  System.out.println("end");
              }
          }
      }
      
    • ThreadB.java

       public class ThreadB extends Thread{
        private Object lock;
        public ThreadB(Object lock){
            super();
            this.lock = lock;
        }
        @Override
        public void run(){
            super.run();
            synchronized (lock){
                for (int i = 0; i < 7; i++) {
                    System.out.println(i);
                    try {
                        Thread.sleep(1000);
                    }catch (InterruptedException e){
                        e.printStackTrace();
                    }
                    if (i == 4){
                        lock.notify();
                    }
                }
      
            }
      
        }
      }
      
    • Run.java

      public class Run {
          public static void main(String[] args) {
              Object lock = new Object();
              ThreadA A = new ThreadA(lock);
              A.setName("A");
              A.start();
              ThreadB B = new ThreadB(lock);
              B.setName("B");
              B.start();
      
          }
      }
      

      等待与通知

      可以看到,在 i = 4 时调用notify( ),并不会马上释放锁。

  • 线程状态

    • NEW 新建

    • RUNNABLE 就绪状态

      线程可以 CPU 调度,若该线程抢占到了 CPU 资源,那么就可以进入 RUNNING 状态。

      进入 Running 状态的情况

      • Sleep() 休眠已结束。
      • 线程调用的阻塞 IO 已返回,阻塞方法执行完毕。
      • 线程成功获得了锁之后。
      • 正在等待通知的线程,被其他线程通知,并且有最高的优先级来执行。
      • 处于挂起状态的线程调用了 resume 方法。
    • BLOCKED 阻塞

      比如遇到 IO 操作,当前线程进入阻塞状态,把 CPU 时间片让给其他线程。Blocked 状态结束后,线程进入Runnable 状态。

      进入 Blocked 状态的情况

      • 线程调用 Sleep() ,主动放弃占用 CPU 资源。
      • 线程调用了 阻塞式 IO,在该方法被返回前,该线程被阻塞。
      • 线程试图获得同步监视器,但该同步监视器正在被其他线程持有。(轮不到获得同步锁)
      • 多个线程同时收到可触发的通知,优先级高的先获得锁得以进入runable 状态,其他线程处于 blocked 状态。
      • 某个线程调用了 其他线程的 suspend 方法将其他线程挂起,被挂起者进入 Blocked 状态。该方法容易产生死锁
    • WAITING 等待

      该线程由其他线程的 notify 来唤醒。

    • TIMED_WAITING 有限时间的等待。

      到达一定时间后自我唤醒或者受到他人唤醒。

    • TERMINATED 终止

      run() 方法执行完毕 或者 main 方法完成之后,线程进入terminated 状态。

  • 锁对象

    每个锁对象都有两个队列,一个是就绪队列,一个是阻塞队列。

  • wait/notify 内容补充

    • wait() 会释放锁

    • notify() 不释放锁

    • sleep() 不释放锁

    • wait() 之后的线程,可以被 interrupt() 打断,并且释放锁。

    • 通知过早

      若通知过早,会打乱程序运行逻辑。

      public class MyRun {
          private String lock = new String("");
          public Runnable runnableA = new Runnable() {
              @Override
              public void run() {
              try{
                  synchronized (lock){
                      System.out.println("Start wait");
                      lock.wait();
                      System.out.println("End wait");
                  }
              }catch (Exception e){
      
              }
              }
          };
          public Runnable runnableB = new Runnable() {
              @Override
              public void run() {
                  synchronized (lock){
                      System.out.println("Start notify");
                      lock.notify();
                      System.out.println("End notify");
                  }
              }
          };
      }
      

      过早通知如下

      
      public static void main(String[] args) {
          MyRun run = new MyRun();
          Thread B = new Thread(run.runnableB);
          B.setName("B");
          Thread A = new Thread(run.runnableA);
          A.setName("A");
      
          B.start();  //先通知
          A.start();  //再等待
      }
      
      

      过早通知

      正常顺序启动

      public static void main(String[] args) {
          MyRun run = new MyRun();
          Thread B = new Thread(run.runnableB);
          B.setName("B");
          Thread A = new Thread(run.runnableA);
          A.setName("A");
      
          A.start();  //先等待
          B.start();  //再通知
      }
      

      正常等待后通知

    • wait 的条件发生变化会导致程序逻辑的混乱

    小结

    • wait() 与 Thread.sleep()的区别 wait() 会释放同步锁,而Thread.sleep() 不会释放锁。