Java多线程(7)——线程的状态转换、线程间的通信——等待唤醒机制

目录

1.线程的状态转换

2.线程间的通信——等待唤醒机制

2.1 线程间的通信概念

2.2 等待唤醒机制

(1)什么是等待唤醒机制

(2)等待与唤醒机制涉及的三个方法

          1)等待:wait

          2)唤醒:notify和notifyAll

(3)锁池(EntryList)和等待池(WaitSet)

         1)锁池(EntryList)

         2)等待池(WaitSet)


1.线程的状态转换

在Thread内部类java.lang.Thread.State 这个枚举中给出了六种线程状态:

Java多线程(7)——线程的状态转换、线程间的通信——等待唤醒机制_第1张图片

Java多线程(7)——线程的状态转换、线程间的通信——等待唤醒机制_第2张图片

  • new Thread()之后,线程并没有被启动
  • 调用start方法后,线程启动,但不一定会立即执行,进入runnable(可运行)状态中的ready(就绪)状态,CPU调度进入running(运行)状态
  • 在running状态的线程,线程可能发生阻塞,比如在进入synchronized块或方法时,如果获取锁失败,会进入blocked状态,当获取到锁时,会回到ready状态
    • Java多线程(7)——线程的状态转换、线程间的通信——等待唤醒机制_第3张图片
    • 阻塞状态下,具有CPU的执行资格,锁被释放会抢夺锁进行执行
  • blocked状态的时候不可能立即回到running(执行)状态,必须先回到ready状态
  • 运行中的线程还会进入等待状态,
    • 一个是有超时时间的等待(休眠状态),比如调用Object类的wait(long)方法,Thread类的join(long)方法等,从TIMED_WAITING状态可以
    • Java多线程(7)——线程的状态转换、线程间的通信——等待唤醒机制_第4张图片
    • 另外一个是无超时时间的等待(无限等待状态),比如调用Thread类的join()方法或者LockSupport类的park方法或者Object类中的wait()方法
      • Java多线程(7)——线程的状态转换、线程间的通信——等待唤醒机制_第5张图片
    • 这两种等待都可以通过notify或unpark结束等待状态,恢复到ready状态
    • 这两种等待状态放弃了CPU的执行资格,CPU空闲,也不会执行
  • 最终,各种状态下的线程都有可能结束,进入terminated状态(run方法执行完为正常结束)

2.线程间的通信——等待唤醒机制

2.1 线程间的通信概念

  • 概念:多个线程在处理同一个资源,但是处理的动作(线程的任务)却不相同。
    • 比如:线程A用来生成包子的,线程B用来吃包子的,包子可以理解为同一资源,线程A与线程B处理的动作,一个 是生产,一个是消费,那么线程A与线程B之间就存在线程通信问题。

    • Java多线程(7)——线程的状态转换、线程间的通信——等待唤醒机制_第6张图片

为什么要处理线程间通信?

  • 多个线程并发执行时, 在默认情况下CPU是随机切换线程的,当我们需要多个线程来共同完成一件任务,并且我们希望他们有规律的执行, 那么多线程之间需要一些协调通信,以此来帮我们达到多线程共同操作一份数据。

如何保证线程间通信有效利用资源?

  • 多个线程在处理同一个资源,并且任务不同时(如一个生产,一个消费),需要线程通信来帮助解决线程之间对同一个变量的使用或操作。
  • 多个线程在操作同一份数据时, 避免对同一共享变量的争夺。
  • 也就是我们需要通过一定的手段使各个线程能有效的利用资源。而这种手段即—— 等待唤醒机制。

2.2 等待唤醒机制

(1)什么是等待唤醒机制

  • 在一个线程进行了规定操作后,就进入等待状态(wait()), 等待其他线程执行完他们的指定代码过后 再将其唤醒(notify();在有多个线程进行等待时, 如果需要,可以使用 notifyAll()来唤醒所有的等待线程。
  • wait/notify 就是线程间的一种协作机制。

Java多线程(7)——线程的状态转换、线程间的通信——等待唤醒机制_第7张图片

(2)等待与唤醒机制涉及的三个方法

重点强调:以下三个方法wait、notify、notifyAll都是Object的方法

1)等待:wait

Java多线程(7)——线程的状态转换、线程间的通信——等待唤醒机制_第8张图片

  • 调用wait方法,线程主动放弃CPU的执行权,即使CPU空闲也不会被调度,进入WaitSet中(关于WaitSet见下文),最重要的是同时会释放锁

  • wait()会让线程进入无限等待状态(WAITING),只能等待notify或notifyAll来唤醒,即从WaitSet中释放出来,重新进入到调度队列中
  • wait(long)或wait(long,int)会让线程进入计时等待状态(TIMED_WAITING),在时间未到时,它可能被notify或notifyAll唤醒,否则,时间到了,它会自动苏醒,进入调度队列

wait和sleep方法的联系:

  • 会让程序暂停执行放弃CPU执行权,都可以进入有超时时间的等待状态(TIMED_WAITING)

wait和sleep方法的区别:

  • 1.sleep是Thread类的方法,wait是Object类中定义的方法
  • 2.sleep()方法可以在任何地方使用;而wait()方法只能在synchronized方法或synchronized块中使用
  • 3.最核心的区别:Thread.sleep只会让出CPU,不会导致锁行为的改变;Object.wait不仅让出CPU,还会释放已经占有的同步资源锁
  • 4.sleep在调用的时候必须传入时间,时间到之后,线程会自动苏醒;而wait有两种方法:无限等待的wait()和有超时时间的等待wait(long),wait()被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的 notify() 或者 notifyAll() 方法。wait(long)超时后线程会自动苏醒。
  • 5.wait 通常被用于线程间通信,sleep 通常被用于程序暂停执行

示例代码验证:

public class WaitSleepDemo {
    public static void main(String[] args)  {
        //同步锁
        final Object lock = new Object();
        //线程A
        new Thread(()->{
            System.out.println("thread A is waiting to get lock ");
            synchronized (lock){

                //注意:run方法中不能抛出异常
                try {
                    System.out.println("thread A get lock");
                    //用sleep来模仿程序执行逻辑
                    Thread.sleep(20);
                    System.out.println("thread A do wait method");
                    lock.wait(1000);
                    System.out.println("thread A is done");
                }catch (InterruptedException e){
                    e.printStackTrace();
                }

            }
        },"A").start();

        //让主线程在这里睡眠10ms,保证先线程A先执行,保证线程A先拿到lock锁
        //如果这里不加sleep,线程A和线程B执行的顺序不一定
        try {
            Thread.sleep(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        //线程B
        new Thread(()->{
            System.out.println("thread B is waiting to get lock ");
            synchronized (lock){

                //注意:run方法中不能抛出异常
                try {
                    System.out.println("thread B get lock");
                    System.out.println("thread B is sleeping 10 ms");
                    Thread.sleep(10);
                    System.out.println("thread B is done");
                }catch (InterruptedException e){
                    e.printStackTrace();
                }

            }
        },"A").start();
    }
}

Java多线程(7)——线程的状态转换、线程间的通信——等待唤醒机制_第9张图片

  • 线程A先执行获取到lock锁,向下执行,由于线程A执行业务的20ms大于主线程睡眠的10ms,所以线程B早都开始执行了,线程B执行到同步代码块时,由于锁被A占有,B线程进入BLOCKED状态
  • 线程A遇到wait方法,放弃CPU执行权,并且释放lock锁,进入TIMED_WAITING状态
  • 由于线程A释放了lock,线程B这时抢到了lock锁,进入同步代码块执行,直到执行结束
  • 最终A在wait完1秒之后,再次获取到锁继续执行完
public class WaitSleepDemo {
    public static void main(String[] args)  {
        //同步锁
        final Object lock = new Object();
        //线程A
        new Thread(()->{
            System.out.println("thread A is waiting to get lock ");
            synchronized (lock){

                //注意:run方法中不能抛出异常
                try {
                    System.out.println("thread A get lock");
                    //用sleep来模仿程序执行逻辑
                    Thread.sleep(20);
                    System.out.println("thread A do sleep method");
                    Thread.sleep(1000);
                    System.out.println("thread A is done");
                }catch (InterruptedException e){
                    e.printStackTrace();
                }

            }
        },"A").start();

        //让主线程在这里睡眠10ms,保证先线程A先执行,保证线程A先拿到lock锁
        //如果这里不加sleep,线程A和线程B执行的顺序不一定
        try {
            Thread.sleep(20);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        //线程B
        new Thread(()->{
            System.out.println("thread B is waiting to get lock ");
            synchronized (lock){

                //注意:run方法中不能抛出异常
                try {
                    System.out.println("thread B get lock");
                    System.out.println("thread B is waiting 10 ms");
                    lock.wait(10);
                    System.out.println("thread B is done");
                }catch (InterruptedException e){
                    e.printStackTrace();
                }

            }
        },"A").start();
    }
}

Java多线程(7)——线程的状态转换、线程间的通信——等待唤醒机制_第10张图片

  • 线程A先执行获取到lock锁,向下执行,由于线程A执行业务的20ms大于主线程睡眠的10ms,所以线程B早都开始执行了,线程B执行到同步代码块时,由于锁被A占有,B线程进入BLOCKED状态
  • 线程A遇到sleep方法,放弃CPU执行权,但不会释放lock锁,进入TIMED_WAITING状态
  • 此时其实CPU是空闲的,但是由于锁还是被A占有,线程B还是处于BLOCKED状态
  • 线程A执行完之后释放了lock,线程B这时抢到了lock锁,进入同步代码块执行,直到执行结束

以上也是wait只能在同步代码块或方法中使用的原因:因为只有获取锁,才能释放锁

2)唤醒:notify和notifyAll

Java多线程(7)——线程的状态转换、线程间的通信——等待唤醒机制_第11张图片

  • 同wait一样,notify和notifyAll必须由持有锁的对象来调用,即也只能在synchronized代码块或方法中使用
  • 其他详细介绍见下文等待池部分的讲解

(3)锁池(EntryList)和等待池(WaitSet)

Java多线程(7)——线程的状态转换、线程间的通信——等待唤醒机制_第12张图片

对于Java虚拟机中运行程序的每个对象来说,都有两个池——锁池和等待池,而这两个池用于Object类的wait、notify、notifyAll三个方法以及synchronized相关

关于Monitor见我的另一篇文章:https://blog.csdn.net/qq_34805255/article/details/99595116

1)锁池(EntryList)

  • Java中,每个对象都有一个唯一与之对应的内部锁(Monitor)
  • 假设object是任意一个对象,monitor是这个对象对应的内部锁
    • 假设有线程A、B、C同时申请monitor,那么由于任意时刻只有一个线程能够获得(占用/持有)这个锁,因此除了获得了锁的线程(这里假设为线程B)外,其他线程(线程A和C)都会进入BLOCKED状态,这些因申请锁而落选的线程就会被存入object的EntryList中
    • 当monitor被其持有的线程(线程B)释放时,EntryList中的任意一个会被唤醒(线程的状态变为RUNNABLE),这些被唤醒的线程与其他活跃线程(即不处于EntryList之中,且线程的状态为RUNNABlE的线程)再次抢占monitor,这时,被唤醒的线程如果成功申请到monitor,那么该线程就从EntryList中移除,否则,仍然停留在EntryList​​​​​​​中,并再次被阻塞,以等待下一次申请锁的机会

2)等待池(WaitSet)

  • 也叫等待队列
  • 所有对象都有一个WaitSet,WaitSet是一个在执行该实例的wait方法时,暂停执行的线程的集合——线程的休息室
  • 一旦执行wait方法时,线程便会暂时停止操作,进入WaitSet这个休息室。除非发生下列一种情况线程会退出WaitSet外,否则线程会永远被留在这个WaitSet中
    • 1.有其他线程以notify方法唤醒该线程
    • 2.有其他线程以notifyAll方法唤醒该线程
    • 3.有其他线程以interrupt方法唤醒该线程
    • 4.wait方法已经到期

wait方法是将线程放入等待集中

  • obj.wait():暂停当前线程的执行,并将当前线程放入obj的WaitSet中
  • 在一个类中的直接调用wait(),则它进入的是this对象的WaitSet中
  • 要执行wait方法,线程必须持有该对象的锁 ,但如果线程进入等待队列,便会释放锁
  • 图解过程如下:

Java多线程(7)——线程的状态转换、线程间的通信——等待唤醒机制_第13张图片

Java多线程(7)——线程的状态转换、线程间的通信——等待唤醒机制_第14张图片

Java多线程(7)——线程的状态转换、线程间的通信——等待唤醒机制_第15张图片

notify方法是从等待队列中取出线程

  • 锁对象执行notify以后,会从该对象的WaitSet中取出一个线程
  • WaitSet中的线程不止一个的话,在执行notify方法之后,“这时该如何来选择线程”,JVM规范中并没有做出规定,究竟是选择最先wait的线程,还是随机选择,或者采用其他方法要取决于JVM厂商的具体实现,因此,编写程序时需要注意,不要编写依赖于所选线程的程序
  • notify唤醒的线程并不会在执行notify的一瞬间重新运行,因为在执行notify的线程还持有着锁,所以其他线程还无法获取这个实例的锁
  • 图解过程如下:

Java多线程(7)——线程的状态转换、线程间的通信——等待唤醒机制_第16张图片

Java多线程(7)——线程的状态转换、线程间的通信——等待唤醒机制_第17张图片

Java多线程(7)——线程的状态转换、线程间的通信——等待唤醒机制_第18张图片

Java多线程(7)——线程的状态转换、线程间的通信——等待唤醒机制_第19张图片

notifyAll方法是从等待队列中取出所有线程

  • obj.notifyAll:在obj对象的WaitSet中休眠的所有线程都会被唤醒
  • notifyAll:在this对象的WaitSet中休眠的所有线程都会被唤醒
  • notify和notifyAll的区别:
    • notifyAll会让所有处于等待池的线程全部进入锁池去竞争获取锁的机会
      • Java多线程(7)——线程的状态转换、线程间的通信——等待唤醒机制_第20张图片
    • notify只会随机选取一个处于等待池中的线程进入锁池去竞争获取锁的机会
      • Java多线程(7)——线程的状态转换、线程间的通信——等待唤醒机制_第21张图片
  • 同上,刚被唤醒的线程会去获取锁,但是此时正在执行notifyAll的线程正持有着锁,因此刚唤醒的线程会进入阻塞状态,只有在执行notifyAll的线程释放锁之后,其中一个线程才能抢到锁从它当时wait的下一句开始执行

问题1:如果线程未持有锁,会怎么样呢?

  • 如果未持有锁的线程调用wait、notify、notifyAll,将会抛出以下异常
  • Java多线程(7)——线程的状态转换、线程间的通信——等待唤醒机制_第22张图片

问题2:可不可以在这个synchronized代码块中调用另一个synchronized代码块的另一个监视器(即锁)的wait、notify、notifyAll方法?

  • public class WaitSleepDemo {
        public static void main(String[] args)  {
            //同步锁
            final Object lock1 = new Object();
            final Object lock2 = new Object();
            //线程A
            new Thread(()->{
                synchronized (lock1){
                    try {
                        System.out.println("thread A get lock1");
                        System.out.println("thread A do sleep method");
                        Thread.sleep(1000);
                        System.out.println("thread A is done");
                    }catch (InterruptedException e){
                        e.printStackTrace();
                    }
    
                }
            },"A").start();
    
            //让主线程在这里睡眠10ms,保证先线程A先执行,保证线程A先拿到lock锁
            //如果这里不加sleep,线程A和线程B执行的顺序不一定
            try {
                Thread.sleep(20);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
    
            //线程B
            new Thread(()->{
                synchronized (lock2){
    
                    //注意:run方法中不能抛出异常
                    try {
                        System.out.println("thread B get lock2");
                        System.out.println("thread B will wait 10 ms by lock1");
                        lock1.wait(10);
                        System.out.println("thread B is done");
                    }catch (InterruptedException e){
                        e.printStackTrace();
                    }
    
                }
            },"A").start();
        }
    }
    

    Java多线程(7)——线程的状态转换、线程间的通信——等待唤醒机制_第23张图片

  • 线程B中的synchronized代码块执行时,线程B持有的是lock2锁,此时lock2是监视器,而lock1对线程B来说并不是监视器,不被B持有,所以调用lock1的方法,就会抛出未持有锁时同样的异常

  • 结论:不能

问题3:该使用notify方法还是notifyAll方法呢?

  • 由于notify唤醒的线程较少,所以处理速度要比使用notifyAll时块
  • 但使用notify时,如果处理不好,程序便可能会停止,一般来说,使用notifyAll时的代码要比使用notify时的更为健壮
  • 除非开发人员完全理解diamagnetic的含义和范围,否则使用notifyAll更为稳妥

使用notify出现问题的示例:

Java多线程(7)——线程的状态转换、线程间的通信——等待唤醒机制_第24张图片

  • 使用该类时,有时候会出现蛋糕会无法传递的情况,而将上述的notify改为notifyAll则不会出现该问题
  • 出现此问题的情况解释:
  • 待补充

问题4:线程在被从WaitSet中唤醒以后,会重新去竞争锁,那它是否需要从synchronized的开始又去执行一遍?

  • 线程在被唤醒以后,是必须重新获取锁,但是当竞争到锁之后,会从wait()的下一句开始执行,因为在wait的时候,代码执行的地址会被记录的,不可能再返回去执行已经执行过的代码

你可能感兴趣的:(#,Java多线程)