Object.wait()超过了超时

sda*_*bet 7 java

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

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

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

JJF*_*JJF 5

当一个线程在超时或睡眠醒来后实际唤醒时并不准确.睡眠的文件有这个说明

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

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

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

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

  • 我的回答是*不*解释OP添加的附加信息,编辑约30秒等待. (2认同)

t0r*_*r0X 3

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

请参阅 Javadoc 以了解Object.wait(long timeout) 方法

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

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

PS
@nathan-hughes 在对你的问题的评论中提到的引用可能是“wait”方法的 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();
}
Run Code Online (Sandbox Code Playgroud)

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

  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...
}
Run Code Online (Sandbox Code Playgroud)

在这种情况下,即使 T1 已收到通知(或者可能超时),它也必须等待,直到再次获得 'obj' 上的锁,只要复杂操作运行,该锁仍然由 T2 持有(步骤 3 中的步骤 3)。之前的列表)!这可能确实需要...几秒钟或更长时间。

更加复杂:我们返回到最初的简单线程 T1 和 T2,但添加了第三个线程:

// 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...
}
Run Code Online (Sandbox Code Playgroud)

执行顺序大致可以变为:

  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 + 许多:T3 *释放“obj”上的锁。
  7. 500ms + 许多:T1重新获取“obj”上的锁(!),然后退出其同步块并释放“obj”上的锁。

这只是触及“非常复杂的软件”和“高流量”中可能发生的情况的表面。添加更多线程,可能编码不当(例如,在“同步”块中执行过多操作)、高流量,并且您可能很容易出现您提到的过度等待。

选项
如何解决这个问题...取决于软件的目的和复杂性,没有简单的计划。根据现有信息,无法透露更多信息。

也许用笔和纸重新分析代码就足够了,也许分析它可以帮助您找到锁,也许您可​​以通过 JMX 或线程转储(通过 signal、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 不是问题(见下文),并且您已经分析了代码,它“看起来不错”,并且您认为高流量是原因,那么您还可以考虑启用偏向锁定或/和自旋锁定。有关更多详细信息,请参阅Java 7 JVM 选项(文章也包含 Java 8 JVM 选项的链接)。

垃圾收集
顺便说一句,“高流量”应该让我早点问这个:垃圾收集,你监控过吗?如果配置/调整不正确,GC 也可能经常导致非常严重的暂停!(这周我遇到了这样的情况,15-30秒进行完整GC......)