分析 JAVA Object.wait/notify (All) 实现原理

分享 TomDeng ⋅ 于 2017-01-03 13:45:18 ⋅ 最后回复由 十里春风 2017-07-10 16:59:11 ⋅ 7126 阅读

在Java的Object类中有2个不怎么常用的方法:wait()与notify()或notfiyAll(),这两个方法主要用于多线程间的协同处理,即控制线程之间的等待、通知、切换及唤醒。由于现在JAVA平台自身提升了很多并发类与库(JUC),以及很多第三方的开源的并发框架(disruptorakka),因此大部分开发者都很少用这种原始的方法去控制线程的协作,所以对其中的一些细节很少去研究。此文就与大家一起来探究其中的奥秘。
首先让大家分析如下代码的运行结果:

    public static void main(String... args) throws InterruptedException {
        final byte[] lock = new byte[0];

        final Thread thread1 = new Thread("thread1") {
            @Override
            public void run() {
                System.out.println("thread1 is ready!");
                try {
                    Thread.sleep(1000 * 5);
                } catch (InterruptedException e) {
                }

                synchronized (lock) {
                    lock.notify();
                    System.out.println("thread1 is notify, but not exit synchronized!");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                    }
                    System.out.println("thread1 is exit synchronized!");
                }
            }
        };

        final Thread thread2 = new Thread("thread2") {
            @Override
            public void run() {
                System.out.println("thread2 is ready!");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                }

                synchronized (lock) {
                    try {
                        System.out.println("thread2 is waiting!");
                        lock.wait();
                        System.out.println("thread2 is awake!");
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        };

        thread1.start();
        thread2.start();
    }

上段代码运行结果如下:

    thread2 is ready!
    thread1 is ready!
    thread2 is waiting!
    thread1 is notify, but not exit synchronized!
    thread1 is exit synchronized!
    thread2 is awake!

表面上看这个运行结果觉得很正常,也许与你分析的情况也相符合。但是如果我们深入分析一下就会发现以下几个问题:

  • 为何调用wait或notify方法时要加同步锁(synchronized),不加行不行?
  • 调用thread2的lock.wait方法后当前线程还未退出同步代码块,其他线程(thread1)能进入同步块么?
  • 为何调用wait方法有可能抛出InterruptedException异常?
  • 当调用notify/notiflyAll方法后等待中的线程会立刻唤醒么?
  • 当调用notify/notiflyAll方法后是随机从等待线程队列中取一个或按某种规律取一个来执行?
  • wait的线程是否会影响系统加载降低性能?

下面我们就来逐一分析上述问题背后的原因。

为何调用wait或notify方法时要加同步锁(synchronized),不加行不行

从实现上来说,这个锁至关重要,正因为这把锁,才能让整个wait/notify玩转起来,因为它要保证wait()/notify()这个整体代码块的原子性。具体可以看参考文章1

调用thread2的lock.wait方法后当前线程还未退出同步代码块,其他线程(thread1)能进入同步块么

这个问题其实要回答很简单,因为在wait处理过程中会临时释放同步锁(这一点是关键,如果不释放锁那别的线程根本没有机会抢占锁),不过需要注意的是当某个线程调用notify唤起了这个线程的时候,在wait方法退出之前会重新获取这把锁,只有获取了这把锁才会继续执行,想象一下,我们知道wait的方法是被monitorenter和monitorexit包围起来,当我们在执行wait方法过程中如果释放了锁,出来的时候又不拿锁,那在执行到monitorexit指令的时候会发生什么?我们可以从jdk的源代码来看看其中的内幕,具体代码在jdk源代码的objectMonitor类objectMonitor::wait方法中.在线程进行wait的时候,会先放弃到已经抢到的锁——exit(self),而释放锁的前提是先要获取到锁,这也是为什么java wait()方法会IllegalMonitorStateException的原因,接着他会将自己park住(实质是pthread_mutex_lock)。这时候再回到notify的else策略中,选取一个wait的线程进行unpark,被unpark的线程会获得CPU执行,但是是否意味着能获取到锁呢?不是的。我们看看wait的线程被unpark之后,还是需要去竞争锁(因为java wait()方法还是在同步块中的,不去竞争锁会违反同步规则), enter (Self) 或者ReenterI (Self, &node),objectMonitor::enter其实也是monitorenter的入口,objectMonitor::enter中主要是进行tryLock和trySpin来竞争锁,如果还是没有竞争到则会进入休眠。

为何调用wait方法有可能抛出InterruptedException异常

这个异常大家应该都知道,当我们调用了某个线程的interrupt方法时,对应的线程会抛出这个异常,wait方法也不希望破坏这种规则,因此就算当前线程因为wait一直在阻塞,当某个线程希望它起来继续执行的时候,它还是得从阻塞态恢复过来,因此wait方法被唤醒起来的时候会去检测这个状态,当有线程interrupt了它的时候,它就会抛出这个异常从阻塞状态恢复过来。
这里有两点要注意:

  • 如果被interrupt的线程只是创建了,并没有start,那等他start之后进入wait态之后也是不能会恢复的
  • 如果被interrupt的线程已经start了,在进入wait之前,如果有线程调用了其interrupt方法,那这个wait等于什么都没做,会直接跳出来,不会阻塞

当调用notify/notiflyAll方法后等待中的线程会立刻唤醒么

其实这个大家可以验证一下,在notify之后写一些逻辑,看这些逻辑是在其他线程被唤起之前还是之后执行,这个是个细节问题,可能大家并没有关注到这个,其实hotspot里真正的实现是退出同步块的时候才会去真正唤醒对应的线程,不过这个也是个默认策略,也可以改的,在notify之后立马唤醒相关线程。这个也可从jdk源代码的objectMonitor类objectMonitor::notify方法中.在调用otify时的默认策略是Policy == 2(这个值是源码中的初值,可以通过-XX:SyncKnobs来设置),其实对于Policy(1、2、3、4)都是将objectMonitor的ObjectWaiter集合中取出一个等待线程,放入到_EntryList(blocked线程集合,可以参与下次抢锁),只是放入_EntryList的策略不一样,体现为唤醒wait线程的规则不一样。对于默认策略notify在将一个等待线程放入阻塞线程集合之后就退出,因为同步块还没有执行完monitorexit还未来到,锁其实还未释放,所以在打印出“thread1 is exit synchronized!”的时候,thread2线程还是blocked状态(因为thread1还没有退出同步埠)。这里可以发现,对于else的情况,会直接将一个ObjectWaiter进行unpark唤醒操作,但是被唤醒的线程是否立即获取到了锁呢?答案是否定的。

当调用notify/notiflyAll方法后是随机从等待线程队列中取一个或按某种规律取一个来执行

或许大家立马想到这个简单,一个for循环就搞定了,不过在jvm里没实现这么简单,而是借助了monitor_exit,上面我提到了当某个线程从wait状态恢复出来的时候,要先获取锁,然后再退出同步块,所以notifyAll的实现是调用notify的线程在退出其同步块的时候唤醒起最后一个进入wait状态的线程,然后这个线程退出同步块的时候继续唤醒其倒数第二个进入wait状态的线程,依次类推,同样这是一个策略的问题,jvm里提供了挨个直接唤醒线程的参数,这里要分情况:

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

wait的线程是否会影响系统加载降低性能

这个或许是大家比较关心的话题,因为关乎系统性能问题,wait/nofity是通过jvm里的park/unpark机制来实现的,在linux下这种机制又是通过pthread_cond_wait/pthread_cond_signal来玩的,因此当线程进入到wait状态的时候其实是会放弃cpu的,也就是说这类线程是不会占用cpu资源,也不会影响系统加载。

版权声明:原创作品,允许转载,转载时务必以超链接的形式表明出处和作者信息。否则将追究法律责任。来自海牛部落-TomDeng,http://hainiubl.com/topics/29
本帖由 青牛 于 3年前 解除加精
回复数量: 2
  • 青牛 国内首批大数据从业者,就职于金山,担任大数据团队核心研发工程师
    2017-01-03 17:57:00

    谢谢分享,让我对Java线程有更多的了解

  • 十里春风 倒萨
    2017-07-10 16:59:11

    0.0

暂无评论~~
  • 请注意单词拼写,以及中英文排版,参考此页
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`, 更多语法请见这里 Markdown 语法
  • 支持表情,可用Emoji的自动补全, 在输入的时候只需要 ":" 就可以自动提示了 :metal: :point_right: 表情列表 :star: :sparkles:
  • 上传图片, 支持拖拽和剪切板黏贴上传, 格式限制 - jpg, png, gif,教程
  • 发布框支持本地存储功能,会在内容变更时保存,「提交」按钮点击时清空
Ctrl+Enter