Object.wait()超过了超时

时间:2015-12-04 16:54:43

标签: java

有什么可以解释Object.wait(timeout)的持续时间超过提供的超时值?

long start = System.currentTimeMillis();
obj.wait(1000);
long duration = System.currentTimeMillis() - start;
// sometimes (very rarely) duration may exceed 1500

上下文:在一个非常复杂的软件的深处,有一段代码可以生成wait并在持续时间过长的情况下生成警告日志。在流量高的生产环境中,一些日志会报告巨大的等待(例如30秒)。因此,我尝试重现它,了解可能发生的事情以及如何修复/改进它。

3 个答案:

答案 0 :(得分:5)

当一个线程在超时或睡眠唤醒后实际唤醒时并不准确。睡眠文件有这个注释

  

导致当前正在执行的线程进入休眠状态(暂时停止   执行)指定的毫秒数,受制于   系统定时器和调度程序的精度和准确性。线程   不会失去任何监视器的所有权。

通知也受这些差异的影响。

因此它与系统中定时器的精度以及线程有资格再次运行的其他线程或进程正在执行的操作有关。一般规则是超时,因为这是将经过的最小时间量。 Object.notify有一个纳秒的变体,可以让你在过去的时间内获得更精细的控制。

请参阅the Javadoc description了解public final void wait(long timeout, int nanos)

答案 1 :(得分:3)

"用户时间"或者"挂钟时间"花费在"等待(超时)"通常,调用是超时值加上线程重新安排执行和执行的时间。

请参阅Javadoc了解Object.wait(long timeout) method

  

然后重新启用线程T以进行线程调度。然后它以通常的方式与其他线程竞争,以便在对象上进行同步;

因此无法保证"实时"操作,它更像是一种“最好的尝试”,取决于当前的系统负载,也可能取决于应用程序中的其他锁定依赖项。因此,如果系统负载过重,或者您的应用程序处理多个线程,则等待可能需要比超时时间长得多。

<强> PS
@ nathan-hughes在他对你的问题的评论中提到的引用可能是&#34;等待&#34;的Javadoc中的关键句子。方法:The specified amount of real time has elapsed, more or less

<强> PPS
根据您的问题编辑,附加上下文信息(&#39;非常复杂的软件&#39;高流量&#39;,#39;巨大的等待&#39;):您必须找到您的所有用法obj对象作为锁,并确定这些用法如何相互作用。

这可能变得非常复杂。这是一个尝试来草拟一个简单的&#34;可能出现问题的情况,只有两个简单的线程,例如这个:

// thread 1
synchronized (obj) {
    // wait 1000ms
    obj.wait(1000);
}
// check for overwait

// thread 2, after, let's say 500 ms
synchronized (obj) {
    obj.notify();
}

简单的场景,一切都很好,执行顺序大致是:

  1. 0ms:T1获取锁定&#39; obj&#39;
  2. 0ms:T1将自身注册为等待&#39; obj&#39;,并从线程调度中排除。 从线程安排中排除,锁定&#39; obj&#39;再次发布(!)
  3. 500ms:T2获取锁定&#39; obj&#39;,通知一个线程等待通知(根据线程调度设置选择线程),并释放锁定对象&#39; < / LI>
  4. 500ms + X:为线程调度重新启用T1,它等待重新获取锁定&#39; obj&#39; (!),然后它完成了它的阻止并释放锁定对象&#39;
  5. 这些只是2个简单线程和synchronized块。让这个更复杂,代码写得不好。如果第二个线程是这样的话会怎么样:

    // bad variant of thread 2, after, let's say 500 ms
    synchronized (obj) {
        obj.notify();
    
        // do complex operation, taking more than few ms,
        // maybe a heavy SQL query/update...
    }
    

    在这种情况下,即使T1已收到通知(或可能已超时),也必须等到它再次锁定“obj&#39; ,这仍然是只要复杂的操作运行(前一个列表中的第3步),由T2持有!这可能确实需要......秒或更长时间。

    更复杂:我们返回初始的简单线程T1和T2,但添加第3个线程:

    // thread 3, after, let's say also 500 ms
    synchronized (obj) {
        // do complex operation, taking more than few ms,
        // maybe a heavy SQL query/update...
    }
    

    执行顺序可能大致成为:

    1. 0ms:T1获取锁定&#39; obj&#39;
    2. 0ms:T1将自身注册为等待&#39; obj&#39;,并从线程调度中排除。 从线程安排中排除,锁定&#39; obj&#39;再次发布(!)
    3. 500ms:T2获取锁定&#39; obj&#39;,通知一个线程等待通知(根据线程调度设置选择线程),并释放锁定对象&#39; < / LI>
    4. 500ms + X:为线程调度重新启用T2,但没有锁定&#39; obj&#39; ,因为
    5. 500ms + X:T3由线程调度程序在T1之前调度,并且获取锁定&#39; obj&#39; (!),并开始进行复杂的操作。 T1除了等待之外什么也做不了!
    6. 500ms + MANY:T3 *释放锁定&#39; obj&#39;。
    7. 500ms + MANY:T1 重新获取锁定&#39; obj&#39; (!),然后退出同步的块并释放锁定对象&#39;
    8. 这只是表明您的“非常复杂的软件”中可能会发生的事情,并且流量很高[&#39;添加更多线程,可能编码不当(例如,在同步&#39;块中做太多),流量高,您可能很容易得到您提到的等待。

      选项
      如何解决这个......取决于你的软件的目的和复杂性,没有简单的计划。根据现有信息,不能说更多。

      也许用笔和纸重新分析代码就足够了,也许分析它可以帮助你找到锁,也许你可以通过JMX或线程转储获得有关当前锁的所需信息(通过信号,jconsole,jcmd,jvisualvm ),或通过Java Mission Control和Java Flight Recording监控(我认为...... JDK 7u40以来可用的功能)。

      您在评论中询问Thread.sleep(timeout)是否会有所帮助:如果没有更多信息,就无法说出来。也许它会有所帮助。或者可能是重入锁或其他锁定选项(请参阅包java.util.concurrentjava.util.concurrent.atomicjava.util.concurrent.locks)更合适。这取决于您的代码,用例以及您正在使用的Java版本。

      如果GC不是问题(见下文),并且您已经分析了代码,它看起来很好&#34;,并且您认为高流量是原因,您可能还会考虑启用偏置锁定或/和旋转锁定。有关更多详细信息,请参阅Java 7 JVM options(文章也包含指向Java 8 JVM选项的链接)。

      垃圾收集
      顺便说一下,“高流量”&#39;应该让我先问一下:垃圾收集,你有没有监控它?如果没有正确配置/调整,GC也可能经常导致非常显着的暂停! (我本周有这样的案例,完整的GC需要15-30秒......)

答案 2 :(得分:2)

等待/休眠应该用于确保程序执行的逻辑顺序。无法保证下一次调度线程,这就是为什么等待通常会保持循环。

那就是说,如果你只是想检查超时的原因,那么试着找到拥有锁的线程并分析该线程。

ThreadMXBean bean = ManagementFactory.getThreadMXBean();
ThreadInfo[] ti = bean.getThreadInfo(bean.getAllThreadIds(), true, true);

ThreadInfo对象包含LockInfo,您可以获取锁的hashCode并检查具有匹配哈希码的线程,以获取它所拥有的锁。

除此之外,如果您无法真正修改代码,请尝试在生产中启用JMX(可能需要重新启动)。

以下参数将添加到Java进程

-Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.ssl=false -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.port=8989

Port是JMX的端口。您可以根据需要启用身份验证,然后您还必须提供用户/密码。检查here

启用JMX后,您将能够使用 Jvisualvm jconsole 查看线程的实时状态。这些工具还允许您在单击按钮时执行线程转储。分析线程转储也可能会给你一个线索。

如果您可以使用JVM在服务器上启用远程调试,那么您将能够调试从IDE获取锁定的线程。 以下是Java进程启用远程调试的参数

-Xdebug -Xnoagent -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=8888

这将启用端口8888上的远程调试。 如果您正在使用eclipse,则可以暂停(右键单击暂停调试透视图中的任何预期线程,以查看它当前正在执行的操作并进行调试。

祝你好运!