Java等待唤醒机制wait/notify深入解析

您所在的位置:网站首页 线程等待的方法 Java等待唤醒机制wait/notify深入解析

Java等待唤醒机制wait/notify深入解析

#Java等待唤醒机制wait/notify深入解析| 来源: 网络整理| 查看: 265

          本文将帮助你彻底弄明白Java的wait/notify等待唤醒机制。为了弄明白wait/notify机制,我们需要了解线程通信、volatile和synchronized关键字、wait/notify方法、Object的monitor机制等相关知识。本文将会从这几个方面详细讲解Java的wait/notify等待唤醒机制。

一、线程通信

       如果一个线程从头到尾执行完也不和别的线程打交道的话,那就不会有各种安全性问题了。但是协作越来越成为社会发展的大势,一个大任务拆成若干个小任务之后,各个小任务之间可能也需要相互协作最终才能执行完整个大任务。所以各个线程在执行过程中可以相互通信,所谓通信就是指相互交换一些数据或者发送一些控制指令,比如一个线程给另一个暂停执行的线程发送一个恢复执行的指令。

       wait和notify/notifyAll就是线程通信的一种方式。

       volatile和synchronized

       可变共享变量是天然的通信媒介,也就是说一个线程如果想和另一个线程通信的话,可以修改某个在多线程间共享的变量,另一个线程通过读取这个共享变量来获取通信的内容。

       由于原子性操作、内存可见性和指令重排序的存在,java提供了volatile和synchronized的同步手段来保证通信内容的正确性,假如没有这些同步手段,一个线程的写入不能被另一个线程立即观测到,那这种通信就是不靠谱的。

 

二、wait、notify/notifyAll方法

       当一个线程获取到锁之后,如果发现条件不满足,那就主动让出锁,然后把这个线程放到一个等待队列里等待去,等到某个线程把这个条件完成后,就通知等待队列里的线程他们等待的条件满足了,可以继续运行了。

       如果不同线程有不同的等待条件怎么办,总不能都塞到同一个等待队列里吧?是的,java里规定了每一个锁都对应了一个等待队列,也就是说如果一个线程在获取到锁之后发现某个条件不满足,就主动让出锁然后把这个线程放到与它获取到的锁对应的那个等待队列里,另一个线程在完成对应条件时需要获取同一个锁,在条件完成后通知它获取的锁对应的等待队列。这个过程意味着锁和等待队列建立了一对一关联。

       怎么让出锁并且把线程放到与锁关联的等待队列中以及怎么通知等待队列中的线程,相关条件java已经为我们规定好了。仅就代码层面来说,我们可以理解为,锁其实就是个对象而已。在所有对象的父类Object中定义了这么几个方法:

public final void wait() throws InterruptedException public final void wait(long timeout) throws InterruptedException public final void wait(long timeout, int nanos) throws InterruptedException public final void notify(); public final void notifyAll();

       各个方法的详细说明如下:

       wait():在线程获取到锁后,调用锁对象的本方法,线程释放锁并且把该线程放置到与锁对象关联的等待队列(等待线程池)。

       wait(long timeout):与wait()方法相似,只不过等待指定的毫秒数,如果超过指定时间则自动把该线程从等待队列中移出

       wait(long timeout, int nanos): 与上边的一样,只不过超时时间粒度更小,即指定的毫秒数加纳秒数

       notify(): 唤醒一个在与该锁对象关联的等待队列的线程,一次唤醒一个,而且是任意的(究竟是不是任意的呢?后文会详细介绍)。

       notifyAll():唤醒全部:可以将线程池中的所有wait() 线程都唤醒。

       其实,所谓唤醒的意思就是让等待队列中的线程具备执行资格。必须注意的是,这些方法都是在同步中才有效(为什么呢?下文会详细介绍)。同时这些方法在使用时必须标明所属锁,这样才可以明确出这些方法操作的到底是哪个锁上的线程。

 

       另一个很重要的问题是,notify()会立刻释放锁么?

       notify()或者notifyAll()调用时并不会真正释放对象锁, 必须等到synchronized方法或者语法块执行完才真正释放锁!!!

举个例子:

public void test() {     Object object = new Object();     synchronized (object){         object.notifyAll();         while (true){         }     } }

如上, 虽然调用了notifyAll, 但是紧接着进入了一个死循环。

这会导致一直不能出临界区, 一直不能释放对象锁。

所以,即使它把所有在等待池中的线程都唤醒放到了对象的锁池中,

但是锁池中的所有线程都不会运行,因为他们始终拿不到锁。

 

三、Java对象的monitor机制

Java虚拟机给每个对象和class字节码都设置了一个监听器Monitor,用于检测并发代码的重入,同时在Object类中还提供了notify和wait方法来对线程进行控制。

1、图解monitor机制

【图注解:Java监视器】

       结合上图来分析Object的Monitor机制。

       Monitor可以类比为一个特殊的房间,这个房间中有一些被保护的数据,Monitor保证每次只能有一个线程能进入这个房间进行访问被保护的数据,进入房间即为持有Monitor,退出房间即为释放Monitor。

       当一个线程需要访问受保护的数据(即需要获取对象的Monitor)时,它会首先在entry-set入口队列中排队(这里并不是真正的按照排队顺序),如果没有其他线程正在持有对象的Monitor,那么它会和entry-set队列、wait-set队列中的被唤醒的其他线程进行竞争(即通过CPU调度),选出一个线程来获取对象的Monitor,执行受保护的代码段,执行完毕后释放Monitor,如果已经有线程持有对象的Monitor,那么需要等待其释放Monitor后再进行竞争。

       再说一下wait-set队列。当一个线程拥有Monitor后,经过某些条件的判断(比如用户取钱发现账户没钱),这个时候需要调用Object的wait方法,线程就释放了Monitor,进入wait-set队列,等待Object的notify方法(比如用户向账户里面存钱)。当该对象调用了notify方法或者notifyAll方法后,wait-set中的线程就会被唤醒,然后在wait-set队列中被唤醒的线程和entry-set队列中的线程一起通过CPU调度来竞争对象的Monitor,最终只有一个线程能获取对象的Monitor。

       需要注意的是:

    【1】当一个线程在wait-set中被唤醒后,并不一定会立刻获取Monitor,它需要和其他线程去竞争

    【2】如果一个线程是从wait-set队列中唤醒后,获取到的Monitor,它会去读取它自己保存的PC计数器中的地址,从它调用wait方法的地方开始执行。

    【3】拥有monitor的是线程

    【4】同时只能有一个线程可以获取某个对象的monitor

    【5】一个线程通过调用某个对象的wait()方法释放该对象的monitor并进入等待队列,直到其他线程获取了被该线程释放的monitor并调用该对象的notify()或者notifyAll()后再次竞争获取该对象的monitor。

    【6】只有拥有该对象monitor的线程才可以调用该对象的notify()和notifyAll()方法。如果没有该对象monitor的线程调用了该对象的notify()或者notifyAll()方法将会抛出java.lang.IllegalMonitorStateException

2、Monitor的实现

       java中每个对象都有唯一的一个monitor,可以通过synchronized关键字实现线程同步来获取对象的Monitor。

       先来看下利用synchronized实现同步的基础:Java中的每个对象都可以作为锁。具体表现为以下三种形式:

      【1】对于普通同步方法,锁是当前实例对象。       【2】对于静态同步方法,锁是当前类的Class对象。       【3】对于同步方法块,锁是Synchonized括号里配置的对象。    

    三种方式具体代码实例如下:

同步代码块

synchronized(Obejct obj) {     //同步代码块     ... }

       上述代码表示在进入同步代码块之前,先要去获取obj的Monitor,如果已经被其他线程获取了,那么当前线程必须等待直至其他线程释放obj的Monitor

这里的obj可以是类.class,表示需要去获取该类的字节码的Monitor,获取后,其他线程无法再去获取到class字节码的Monitor了,即无法访问属于类的同步的静态方法了,但是对于对象的实例方法的访问不受影响

 

       同步方法

public class Test {     public static Test instance;     public int val;     public synchronized void set(int val) {         this.val = val;     }     public static synchronized void set(Test instance) {         Test.instance = instance;     } }

       上述使用了synchronized分别修饰了非静态方法和静态方法。

       非静态方法可以理解为,需要获取当前对象this的Monitor,获取后,其他需要获取该对象的Monitor的线程会被堵塞。

       静态方法可以理解为,需要获取该类字节码的Monitor(因为static方法不属于任何对象,而是属于类的方法),获取后,其他需要获取字节码的Monitor的线程会被堵塞。

     

 

3、图解释放锁和获取锁的过程

       调用对象obj的wait(), notify()方法前,必须获得obj锁,也就是必须写在synchronized(obj) 代码段内。

       一个对象释放锁和获取锁的过程如下:

(同步队列、入口队列、锁池是一个意思)      

       过程分析:

    【1】线程1获取对象A的锁,正在使用对象A。

    【2】线程1调用对象A的wait()方法。

    【3】线程1释放对象A的锁,并马上进入等待队列。

    【4】锁池(入口队列、同步队列)里面的对象争抢对象A的锁。

    【5】线程5获得对象A的锁,进入synchronized块,使用对象A。

    【6】线程5调用对象A的notifyAll()方法,唤醒所有线程,所有线程进入同步队列。若线程5调用对象A的notify()方法,则唤醒一个线程,不知道会唤醒谁,被唤醒的那个线程进入同步队列。

    【7】notifyAll()方法所在synchronized结束,线程5释放对象A的锁。

    【8】同步队列的线程争抢对象锁,但线程1什么时候能抢到就不知道了。

 

       同步队列状态

    【1】当前线程想调用对象A的同步方法时,发现对象A的锁被别的线程占有,此时当前线程进入同步队列。简言之,同步队列里面放的都是想争夺对象锁的线程。

    【2】当一个线程1被另外一个线程2唤醒时,1线程进入同步队列,去争夺对象锁。

    【3】同步队列是在同步的环境下才有的概念,一个对象对应一个同步队列。

    【4】线程等待时间到了或被notify/notifyAll唤醒后,会进入同步队列竞争锁,如果获得锁,进入RUNNABLE状态,否则进入BLOCKED状态等待获取锁。

 

四、随机唤醒

       等待队列里许许多多的线程都wait()在一个对象上,此时某一线程调用了对象的notify()方法,那唤醒的到底是哪个线程?随机?队列FIFO?or sth else?Java文档就简单的写了句:选择是任意性的(The choice is arbitrary and occurs at the discretion of the implementation)。

       既然官方文档都写了是任意的,那么真的是任意的吗?

感兴趣的key参考下面的文章。这里卖个关子,不说结论。

13.1 大佬问我: notify()是随机唤醒线程么? - 简书 (jianshu.com)

 

五、为什么wait和notify方法要在同步块中调用?

       先回答问题

       为什么wait()必须在同步(Synchronized)方法/代码块中调用?

       答:调用wait()就是释放锁,释放锁的前提是必须要先获得锁,先获得锁才能释放锁。

       为什么notify(),notifyAll()必须在同步(Synchronized)方法/代码块中调用?

       notify(),notifyAll()是将锁交给含有wait()方法的线程,让其继续执行下去,如果自身没有锁,怎么叫把锁交给其他线程呢;(本质是让处于入口队列的线程竞争锁)

 

       下面来详细说明。

       首先,要明白,每个Java对象都有唯一一个监视器monitor,这个监视器由三部分组成(一个独占锁,一个入口队列,一个等待队列)。注意是一个对象只能有一个独占锁,但是任意线程线程都可以拥有这个独占锁。

      对于对象的非同步方法而言,任意时刻可以有任意个线程调用该方法。(即普通方法同一时刻可以有多个线程调用)

       对于对象的同步方法而言,只有拥有这个对象的独占锁才能调用这个同步方法。如果这个独占锁被其他线程占用,那么另外一个调用该同步方法的线程就会处于阻塞状态,此线程进入入口队列。

       若一个拥有该独占锁的线程调用该对象同步方法的wait()方法,则该线程会释放独占锁,并加入对象的等待队列;(为什么使用wait()?希望某个变量被设置之后再执行,notify()通知变量已经被设置。)

       某个线程调用notify(),notifyAll()方法是将等待队列的线程转移到入口队列,然后让他们竞争锁,所以这个调用线程本身必须拥有锁。

 

六、wait/notify应用举例:生产者消费者模型

       下面,在不考虑实用性等前提下,我们会实现一个最简单的生产者、消费者模型,仅仅只用来理解wait/notify的机制。

       在这个例子里,将启动一个生产者线程、一个消费者线程。生产者检测到有产品可供消费时,通知消费者(notify)进行消费,同时自己进入等待状态(wait),如果检测到没有产品可供消费,则进行生产。消费者检测到有产品可供消费时,则进行消费,消费结束没通知生产者进行生产,如果检测到没有产品可供消费,自然也通知生产者进行生产。

       也就是说,生产者线程和消费者线程会互相等待和互相通知。他们会争夺同一个对象obj的锁,实现线程之间的通信。

Product类:

public class Product { private static Integer count = 0; public static void add() { count++; } public static void delete() { count--; } public static Integer getCount(){ return count; } }

Produce类:

public class Produce implements Runnable { private Object object; public Produce(Object object) { this.object = object; } @Override public void run() { synchronized (object) { System.out.println("++++ 进入生产者线程"); System.out.println("++++ 产品数量:" + Product.getCount()); while (true) { try { TimeUnit.MILLISECONDS.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } if (Product.getCount()


【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3