(2.1.27.7)Java并发编程:Object.wait/notify

版权声明:本文为博主原创文章,转载请注明出处 https://blog.csdn.net/fei20121106/article/details/83268434

Java Object对象中的wait,notify,notifyAll是定义在Object类的实例方法,用于控制线程状态

三个方法都必须在synchronized 同步关键字所限定的作用域中调用,否则会报错java.lang.IllegalMonitorStateException ,意思是因为没有同步,所以线程对对象锁的状态是不确定的,不能调用这些方法。

  • wait
    • 表示持有对象锁的线程A准备释放对象锁权限,释放cpu资源并进入等待。
  • notify
    • 表示持有对象锁的线程A准备释放对象锁权限,通知jvm唤醒某个随机竞争该对象锁的线程X。
    • 线程A synchronized 代码作用域结束后,线程X直接获得对象锁权限,其他竞争线程继续等待(即使线程X同步完毕,释放对象锁,其他竞争线程仍然等待,直至有新的notify ,notifyAll被调用)。
  • notifyAll
    • 表示持有对象锁的线程A准备释放对象锁权限,通知jvm唤醒所有竞争该对象锁的线程
    • 线程A synchronized 代码作用域结束后,jvm通过算法将对象锁权限指派给某个线程X,所有被唤醒的线程不再等待。线程X synchronized 代码作用域结束后,之前所有被唤醒的线程都有可能获得该对象锁权限,这个由JVM算法决定

我们不禁产生这样的疑问:

  1. 进入wait/notify方法之前,为什么要获取synchronized锁?
  2. 线程A获取了synchronized锁,执行wait方法并挂起,线程B又如何再次获取锁?

一、示例

public class WaitNotifyCase {
    public static void main(String[] args) {
        final Object lock = new Object();

        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("thread A is waiting to get lock");
                synchronized (lock) {
                    try {
                        System.out.println("thread A get lock");
                        TimeUnit.SECONDS.sleep(1);
                        System.out.println("thread A do wait method");
                        lock.wait();
                        System.out.println("wait end");
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("thread B is waiting to get lock");
                synchronized (lock) {
                    System.out.println("thread B get lock");
                    try {
                        TimeUnit.SECONDS.sleep(5);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    lock.notify();
                    System.out.println("thread B do notify method");
                }
            }
        }).start();
    }
}


执行结果:

thread A is waiting to get lock
thread A get lock
thread B is waiting to get lock
thread A do wait method
thread B get lock
thread B do notify method
wait end

由同一个lock对象调用wait、notify方法。
1、当线程A执行wait方法时,该线程会被挂起;
2、当线程B执行notify方法时,会唤醒一个被挂起的线程A;

lock对象、线程A和线程B三者是一种什么关系?根据上面的结论,可以想象一个场景:
1、lock对象维护了一个等待队列list;
2、线程A中执行lock的wait方法,把线程A保存到list中;
3、线程B中执行lock的notify方法,从等待队列中取出线程A继续执行;

二、为什么要使用synchronized?

我们知道wait/notify是为了线程间协作而设计的,当我们执行wait的时候让线程挂起,当执行notify的时候唤醒其中一个挂起的线程,那需要有个地方来保存对象和线程之间的映射关系(可以想象一个map,key是对象,value是一个线程列表)

当调用这个对象的wait方法时,将当前线程放到这个线程列表里,当调用这个对象的notify方法时从这个线程列表里取出一个来让其继续执行

这样看来是可行的,也比较简单,那现在的问题这种映射关系放到哪里?

lock.wait()方法通过调用native方法wait(0)实现,其中接口注释中有这么一句:

The current thread must own this object’s monitor.

表示线程执行lock.wait()方法时,必须持有该lock对象的ObjectMonitor(前文已经提及,每个锁对象(这里指已经升级为重量级锁的对象)都有一个ObjectMonitor(对象监视器)。也就是说每个线程获取锁对象都会通过ObjectMonitor)

如果wait方法在synchronized代码中执行,该线程很显然已经持有了monitor。

从而包含了“ 进入wait/notify方法之前,为什么要获取synchronized锁”,synchronized代码块通过javap生成的字节码中包含 *monitorentermonitorexit指令。其中执行monitorenter指令可以获取对象的monitor

在这里插入图片描述
【为什么要使用synchronized】

三、代码执行过程分析

  1. 在多核环境下,线程A和B有可能同时执行monitorenter指令,并获取lock对象关联的monitor,只有一个线程可以和monitor建立关联,假设线程A执行加锁成功;
  2. 线程B竞争加锁失败,进入等待队列进行等待;
  3. 线程A继续执行,当执行到wait方法

3.1 Object.wait方法实现

wait方法会将当前线程放入wait set,等待被唤醒,并放弃lock对象上的所有同步声明,意味着"线程A释放了锁,线程B可以重新执行加锁操作"

Object.wait()在使用时通常要判断是否满足某个条件,不满足某个外部条件cond时调用wait(),来让线程阻塞同时释放被synchronized锁定的mutex;从这个过程看来Object.wait()实际上是起到条件变量的作用

  1. wait()内部实际上先将synchronized锁定的锁释放
  2. 之后将当前线程阻塞在某个内置的条件condition上(注意:此condition为内置的,与外部判断的条件cond并非同一个,外部的cond需要程序员根据程序逻辑来判断改变,而这个condition只能被Object.notify()/notifyAll()改变),直到内置条件condition被Object.notify()/notifyAll()修改时才会重新锁定该mutex,继续执行wait()后的代码。
wait() {
    unlock(mutex);//解锁mutex
    wait_condition(condition);//等待内置条件变量condition
    lock(mutex);//竞争锁
}

lock.wait()方法最终通过ObjectMonitor的void wait(jlong millis, bool interruptable, TRAPS);实现:

1、将当前线程封装成ObjectWaiter对象node;
在这里插入图片描述
【wait方法实现1】

2、通过ObjectMonitor::AddWaiter方法将node添加到_WaitSet列表中;

在这里插入图片描述
【wait方法实现2】

3、通过ObjectMonitor::exit方法释放当前的ObjectMonitor对象,这样其它竞争线程就可以获取该ObjectMonitor对象。

在这里插入图片描述
【wait方法实现3】

4、最终底层的park方法会挂起线程;
5、ObjectMonitor中的其他竞争线程被CPU自动唤醒和选择某个启动执行

3.2 Object.notify()/notifyAll方法实现

Object.notify()/notifyAll()实际上只起到一个sinal内置条件变量的作用,调用Object.notify()/notifyAll()之后,这个时候其他处于wait()中的线程所等待的内置条件变量已经满足,但是由于wait()中仍然需要lock mutex

而在Object.notify()/notifyAll()中没有把mutex释放掉,故阻塞在wait()处的线程继续等待,但等待的条件不再是内置条件变量而是锁mutex;直到synchronized代码块结束时,由于会自动释放被synchronized锁定的mutex,故此时所有在wait()中等待mutex的线程开始竞争mutex,得到该mutex的会继续执行,否则继续等待mutex

obj.notify()/notifyAll(){
    condition=true;//只起到把内置条件变量置为true的作用
}
  • lock.notify()方法最终通过ObjectMonitor的void notify(TRAPS)实现:

    1. 如果当前_WaitSet为空,即没有正在等待的线程,则直接返回;
    2. 通过ObjectMonitor::DequeueWaiter方法,获取_WaitSet列表中的第一个ObjectWaiter节点,实现也很简单。这里需要注意的是,在jdk的notify方法注释是随机唤醒一个线程,其实是第一个ObjectWaiter节点
    3. 根据不同的策略,将取出来的ObjectWaiter节点,加入到_EntryList或则通过Atomic::cmpxchg_ptr指令进行自旋操作cxq,具体代码实现有点长,这里就不贴了,有兴趣的同学可以看objectMonitor::notify方法;

    在这里插入图片描述
    【Object.notify()notifyAll方法实现】

  • lock.notifyAll()方法最终通过ObjectMonitor的void notifyAll(TRAPS)实现:

    1. 通过for循环取出_WaitSet的ObjectWaiter节点,并根据不同策略,加入到_EntryList或则进行自旋操作。

从JVM的方法实现中,可以发现:notify和notifyAll并不会释放所占有的ObjectMonitor对象. 其实真正释放ObjectMonitor对象的时间点是在执行monitorexit指令,一旦释放ObjectMonitor对象了,entry set中ObjectWaiter节点所保存的线程就可以开始竞争ObjectMonitor对象进行加锁操作了。

  • 被notify(All)的线程有规律吗
    • 如果是通过notify来唤起的线程,那先进入wait的线程会先被唤起来
    • 如果是通过nootifyAll唤起的线程,默认情况是最后进入的会先被唤起来,即LIFO的策略

猜你喜欢

转载自blog.csdn.net/fei20121106/article/details/83268434