美文网首页jvm技术收藏java面试
JVM源码分析之Object.wait/notify实现

JVM源码分析之Object.wait/notify实现

作者: 美团Java | 来源:发表于2016-11-18 19:26 被阅读11921次

    简书 占小狼
    转载请注明原创出处,谢谢!

    最简单的东西,往往包含了最复杂的实现,因为需要为上层的存在提供一个稳定的基础,Object作为java中所有对象的基类,其存在的价值不言而喻,其中wait和notify方法的实现多线程协作提供了保证。

    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继续执行;
    当然了,Hotspot实现不可能这么简单。

    上述代码中,存在多个疑问:

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

    为什么要使用synchronized?
    static void Sort(int [] array) {
        // synchronize this operation so that some other thread can't
        // manipulate the array while we are sorting it. This assumes that other
        // threads also synchronize their accesses to the array.
        synchronized(array) {
            // now sort elements in array
        }
    }
    

    synchronized代码块通过javap生成的字节码中包含 ** monitorenter ** 和 ** monitorexit **指令。


    执行monitorenter指令可以获取对象的monitor,而lock.wait()方法通过调用native方法wait(0)实现,其中接口注释中有这么一句:

    The current thread must own this object's monitor.

    表示线程执行lock.wait()方法时,必须持有该lock对象的monitor,如果wait方法在synchronized代码中执行,该线程很显然已经持有了monitor。

    代码执行过程分析

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

    This method causes the current thread to place itself in the wait set for this object and then to relinquish any and all synchronization claims on this object.

    wait方法会将当前线程放入wait set,等待被唤醒,并放弃lock对象上的所有同步声明,意味着线程A释放了锁,线程B可以重新执行加锁操作,不过又有一个疑问:在线程A的wait方法释放锁,到线程B获取锁,这期间发生了什么?线程B是如何知道线程A已经释放了锁?好迷茫....

    4、线程B执行加锁操作成功,对于notify方法,JDK注释:notify方法会选择wait set中任意一个线程进行唤醒;

    Wakes up a single thread that is waiting on this object's monitor. If any threads are waiting on this object, one of them is chosen to be awakened. The choice is arbitrary and occurs at the discretion of the implementation

    notifyAll方法的注释:notifyAll方法会唤醒monitor的wait set中所有线程。

    Wakes up all threads that are waiting on this object's monitor.

    5、执行完notify方法,并不会立马唤醒等待线程,在notify方法后面加一段sleep代码就可以看到效果,如果线程B执行完notify方法之后sleep 5s,在这段时间内,线程B依旧持有monitor,线程A只能继续等待;

    那么wait set的线程什么时候会被唤醒?

    想要解答这些疑问, 需要分析jvm的相关实现,本文以HotSpot虚拟机1.7版本为例

    什么是monitor?

    在HotSpot虚拟机中,monitor采用ObjectMonitor实现。


    每个线程都有两个ObjectMonitor对象列表,分别为free和used列表,如果当前free列表为空,线程将向全局global list请求分配ObjectMonitor。

    ObjectMonitor对象中有两个队列:_WaitSet 和 _EntryList,用来保存ObjectWaiter对象列表;_owner指向获得ObjectMonitor对象的线程。


    **_WaitSet ** :处于wait状态的线程,会被加入到wait set;
    _EntryList:处于等待锁block状态的线程,会被加入到entry set;

    ObjectWaiter

    ObjectWaiter对象是双向链表结构,保存了_thread(当前线程)以及当前的状态TState等数据, 每个等待锁的线程都会被封装成ObjectWaiter对象。

    wait方法实现

    lock.wait()方法最终通过ObjectMonitor的void wait(jlong millis, bool interruptable, TRAPS);实现:
    1、将当前线程封装成ObjectWaiter对象node;

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

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

    4、最终底层的park方法会挂起线程;

    notify方法实现

    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方法;

    notifyAll方法实现

    lock.notifyAll()方法最终通过ObjectMonitor的void notifyAll(TRAPS)实现:
    通过for循环取出_WaitSet的ObjectWaiter节点,并根据不同策略,加入到_EntryList或则进行自旋操作。

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


    我是占小狼
    坐标魔都,白天是上班族,晚上是知识的分享者
    如果读完觉得有收获的话,欢迎点赞加关注


    我的微信公众号

    相关文章

      网友评论

      • hellosj_601b:有个疑问,一般synchronized释放锁在括号的方法体之后,难道是object.wait特别还是我理解有问题,谢了
        往之farmer:@尹晓乘 notify方法调用后,wait的线程被唤醒后就能拿到锁了啊
        尹晓乘:@蘑菇街_往之 应该是放入_EntryList吧,这个时候唤醒了又拿不到锁,意义何在?我比较有疑问的是notify的几种策略,这篇文章没有解析,跳过了,貌似其中有策略是唤醒线程....
        往之farmer:object.wait的时候会释放锁,当被notify唤醒的时候会重新竞争锁
      • yayaya333:请问下,线程挂起怎么理解
        往之farmer:操作系统层用futex的相关方法,将当期线程从活动队列移动到挂起队列
        美团Java:@yayaya333 unsafe类的park方法了解一下
      • 0701fa97be25:我理解objectMonitor就是以object的维度写了一个C++版本的AQS,而ReentrantLock是以state维度写的一个AQS,但是ReentrantLock是用CAS保证FIFO队列并发场景下能够正常,而objectMonitor并没有使用CAS(看你贴的代码而言),请问JNI的这些方法,是如何保证CAS的呢?
        0701fa97be25:@猴子007 希望美团同事解答下,不胜感激
        0701fa97be25:@猴子007 大神,不太懂。JNI的fence技术??就是同一个JVM调用,只能有一个线程调用JNI的方法,难道是所有线程调用JNI进入使用同一个本地方法栈来保证的???
        猴子007:fence
      • felayman:转载了,写的不错
      • zh_way:看到里面写到最终还是通过park方法来挂起当前线程,这里是指LockSupport中的Lock对象吗,那是不是说处于wait状态的线程可以通过LockSupport.unpark唤醒呢,貌似不行
        冉桓彬:LockSupport是java层面, wait的park是native层面
      • 学而不思则罔_思而不学则殆:@占小狼
        1、在多核环境下,线程A和B有可能同时执行monitorenter指令,并获取lock对象关联的monitor,只有一个线程可以和monitor建立关联,假设线程A执行加锁成功;
        2、线程B竞争加锁失败,进入等待队列进行等待;

        第二应该不是进入_WaitSet 吧,是进入_EntrySet吧?
        evanwo:@学而不思则罔_思而不学则殆 进入entryset
      • 玖柒叁:等待队列会按照线程优先级来排列吗???
        美团Java:@wwwyn666 嗯
      • 布吉刀:是要等到sync之后?
      • 布吉刀:如果线程B执行完notify方法之后sleep 5s,在这段时间内,线程B依旧持有monitor, 怎么理解?
        莫那一鲁道:仍然持有锁,也就是只有出了同步块,另一个线程才会被唤醒。
      • 现代愚公BevisWu:有一个疑问:假如现在有线程A、线程B、线程C同时在争夺lock1。1.此时线程B比较幸运首先获取了lock1,此时线程A和线程C未获取到锁,此时状态为blocked,放在entryllist里。2.然后线程B发现有在等待加一个不可预期的资源,避免忙等,所以调用了lock1.wait(),此时线程B的状态变为waiting,底层会把线程B放到wait set里,此时线程C又获得了lock1,此时三个线程的状态是线程A blocked(entrylist里),线程B waiting(wait set里),线程C runnable.3.线程C 在线程里调用lock1.nofityall, 我这里有疑问是:当线程C调用nofityall后,是先把线程B从wait set里移出后加到entrylist里,此时entrylist里有线程A和线程B,然后一起竞争锁吗?
        尹晓乘:@myfast2009 以前我也有疑问,但是看了ReentrantLock实现公平锁源码之后,我就发现,只要入了队列,再出队都是有先后顺序,因为它们本身入队出队就是按照链表的结构来的,AQS实现公平锁非公平锁一旦入队挂起,再出队也是按照顺序,公平与否是针对入队前而言的。
        书唐瑞:我以前也有过这个疑惑,今天特意测试了一下,场景和你描述的是一样的,最终获取锁的前后顺序一定是:B在前,A在后。它们不会一起竞争锁,因为它们在同步队列中,虽然都处于BLOCKED状态,一定是一个接着一个获取锁(一个接着一个出队列)。结论就是它们一定是有顺序的,虽然一起竞争,但还是有先后顺序,而不是同时竞争。
      • 竹林闲人:能否把参考资料列出来啊(如entry set线程活动图,挺想知道这张图来自哪里),我看楼主的风格一般不写参考资料,如果确实没有参考其他资料,就当我没说
        83b3ccec058e:@盖聂_499e :smile: 我也想问呢~多谢告知~
        4156d67054f9:@walkingCoffee 《深入java虚拟机》第20章
        zoterap:应该是公司的内部资料的,不能分享的
      • ce5a182db8aa:写的真好,先收藏着,留着以后再看。
        美团Java:@walkingCoffee 有可能啊
        zoterap:@辰夕小狼睡不醒 那以后就不会看了
        美团Java:@辰夕小狼睡不醒 :smile:
      • 此鱼不得水:关注了部分你写的文章,觉得很赞。 :+1:
      • widon:写的详细
        widon:谢谢!大神!
        美团Java:@widon 有帮到你理解就好
      • cbbcce60a9ca:有几点疑问,请大神帮忙解释啊
        1.synchronized object.wait从本质上或从底层代码实现来说是不是两种同类型的锁,synchronized是同步锁,wait当时是信号量机制?
        2synchronized难道不是多线程有效的,如果一个线程在同步,另外一个线程加锁失败等待,不是多核就被浪费了,synchronized应该不是全局锁吧?
        3.请大神说说synchronized和waitnitify两种不同实现的基理吧:smile:
        zzmarquis:2、锁一般是不得已在需要保证数据一致性的情况下才使用的策略,锁一般是会导致多核“被浪费”的,所以大并发情况下一般能不用锁的地方就尽量不用锁。
      • jsondream:感谢分享,学习了
      • 588fd0c11960:很棒呐,一直在关注
        美团Java:@渣渣高 谢谢支持:smile:

      本文标题:JVM源码分析之Object.wait/notify实现

      本文链接:https://www.haomeiwen.com/subject/pkmdpttx.html