Object.wait()超过了超时

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

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

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

通过“等待(超时)”调用所花费的“用户时间”或“挂钟时间”通常是超时值加上线程重新调度执行和执行之前的时间。

有关Object.wait(long timeout)方法,请参阅Javadoc:

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

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

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

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

这可能变得非常复杂。 这里尝试绘制一个可能出错的“简单”场景,只有两个普通线程,例如:

 // 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获取’obj’上的锁定
  2. 0ms:T1将自身注册为等待’obj’,并从线程调度中排除。 从线程调度中排除,“obj”上的锁定再次被释放(!)
  3. 500ms:T2获取’obj’上的锁,通知一个线程等待通知(根据线程调度设置选择线程),并释放’obj’上的锁
  4. 500ms + X:为线程调度重新启用T1,它等待它重新获取’obj’(!)上的锁 ,然后它完成它的块并释放’obj’上的锁。

这些只是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’的锁定 ,只要复杂的操作运行,它仍然由T2保持(步骤3中的上一个清单)! 这可能确实需要……秒或更长时间。

更复杂:我们返回初始的简单线程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获取’obj’上的锁定
  2. 0ms:T1将自身注册为等待’obj’,并从线程调度中排除。 从线程调度中排除,“obj”上的锁定再次被释放(!)
  3. 500ms:T2获取’obj’上的锁,通知一个线程等待通知(根据线程调度设置选择线程),并释放’obj’上的锁
  4. 500ms + X:T2为线程调度重新启用,但没有锁定’obj’ ,因为
  5. 500ms + X:T3由T1之前的线程调度程序调度,它获取’obj’(!)上的锁 ,并开始执行它的复杂操作。 T1不能做任何事情,只能等待!
  6. 500ms + MANY:T3 *释放’obj’的锁定。
  7. 500ms + MANY:T1 重新获取’obj’(!)上的锁定 ,然后退出同步块并释放锁定’obj’。

这只是在“非常复杂的软件”中可能发生的事情,“高流量”。 添加更multithreading,可能编码不好(例如,在’同步’块中做太多),流量高,您可能很容易得到您提到的等待。

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

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

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

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

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

当一个线程在超时或睡眠唤醒后实际唤醒时并不准确。 睡眠医生有这个说明

导致当前正在执行的线程hibernate(暂时停止执行)指定的毫秒数,具体取决于系统计时器和调度程序的精度和准确性。 该线程不会失去任何监视器的所有权。

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

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

查看关于public final void wait(long timeout, int nanos) 的Javadoc描述 public final void wait(long timeout, int nanos)

应使用等待/hibernate来确保程序执行的逻辑顺序。 无法保证何时会调度线程,这就是为什么等待通常会保持循环。

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

 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 

端口是JMX的端口。 您可以根据需要启用身份validation,然后您还必须提供用户/密码。 点击这里

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

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

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

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

祝你好运!