Thread.sleep()的实现

32

今天我面试了一名候选人,问了一个相当常见和基础的问题,即 Thread.sleep()Object.wait() 之间的区别。我希望他像这里提到的那样回答,但他说这些方法基本上是一样的,很可能 Thread.sleep 在内部使用了 Object.wait(),但 sleep 本身不需要外部锁。这不是完全正确的答案,因为在JDK 1.6中,该方法具有以下签名。

public static native void sleep(long millis) throws InterruptedException;

但我的第二个想法是这并不荒谬。可以使用定时等待来达到同样的效果。看一下以下代码片段:

public class Thread implements Runnable {
       private final Object sleepLock = new Object();

     // other implementation details are skipped

       public static void sleep(long millis) throws InterruptedException {
            synchronized (getCurrentThread().sleepLock){
                getCurrentThread().sleepLock.wait(millis);
            }
        }
在这种情况下,sleepLock 是一个在 sleep 方法内部用于同步块的对象。我认为 Sun/Oracle 的工程师们都知道奥卡姆剃刀原则,因此 sleep 有本地实现是有意为之的,那么我的问题是为什么它使用本机调用。
我想到的唯一想法是,有人可能会发现像 Thread.sleep(0) 这样的调用很有用。根据这篇文章:

这具有清除当前线程量子并将其放置在其优先级级别的队列末尾的特殊效果。换句话说,在下一个给予 CPU 时间的线程是已运行的相同优先级 (和那些更高优先级的线程) 之前,所有可运行的线程都会得到一个机会。

因此,synchronized 块会给出不必要的开销。 你知道不使用定时等待在 Thread.sleep() 实现中的其他原因吗?

1
wait(millis) 可能会出现虚假唤醒(提前唤醒),而 sleep 不会在未被中断的情况下提前唤醒。 - Peter Lawrey
在调用sleep()时,是否会检查中断标志以抛出异常并返回?还是这个调用也会进入调度程序,然后调度程序知道结束线程的休眠? - klaus johan
我更关心他们是否知道如何正确处理InterruptedException。 - Martin Spamer
4个回答

10
很容易就可以说奥卡姆剃刀是双向的。JDK底层的JVM正常/预期的实现假定大部分时间将Java“线程”绑定到本机线程,并将线程置于睡眠状态是底层平台的基本功能。如果线程代码最终也会是本地的,为什么要在Java中重新实现它呢?最简单的解决方案是使用已经存在的函数。
还有一些其他的考虑因素: 在现代JVM中,未争用的同步是微不足道的,但这并非总是如此。以前获取对象监视器是相当“昂贵”的操作。
如果你在Java代码中实现了线程睡眠,并且你实现的方式没有绑定到本地线程等待,那么操作系统必须继续安排该线程以运行检查是否到了唤醒时间的代码。正如在评论中所讨论的那样,在现代JVM上这显然不成立,但很难说: 1)在Thread类首次指定该方式时可能已经存在并预期发生过什么。 和 2)该断言是否适用于曾经想要在其上实现JVM的所有平台。

@ruakh,我猜测当本地线程在pthread之上实现时,pthread_cond_*waitObject.wait的基础。 - Mike Samuel
人们可能会期望现代的JVM和平台都做到了这一点。然而,OP的问题是“为什么10年前的Sun工程师们会做出这个决定的原因是什么?”,而不是关于现在事物的运作方式。 :) - Affe
@ruakh,当我阅读Affe的回答时,我没有那种印象。 - Mike Samuel
1
不,这意味着Java对象可以封装提供监视器wait()功能的操作系统同步原语。 在object.wait()上阻塞的线程不会被操作系统给予任何CPU,就像睡眠线程一样。 - Martin James
@ruakh 目前肯定不是这样的。答案是对一个假设性问题的推测性回答,关于为什么可能在很久以前做出了决定。我会尝试想出一种重新措辞的方式,以免误导。 - Affe
显示剩余2条评论

7

你知道不使用 Thread.sleep() 实现中的定时等待的其他原因吗?

因为本地线程库提供了完美的睡眠功能:http://www.gnu.org/software/libc/manual/html_node/Sleeping.html

要理解为什么本地线程很重要,请从http://java.sun.com/docs/hotspot/threads/threads.html开始。

版本1.1基于绿色线程,这里不会涉及。 绿色线程是VM内的模拟线程,在采用1.2及以上的本机OS线程模型之前使用。 绿色线程在Linux上可能具有优势(因为您不必为每个本机线程生成进程),但自1.1版本以来,VM技术已经显着发展,多年来的性能提高抹去了绿色线程过去所拥有的任何好处。


2

Thread.sleep()不会被虚假唤醒提前唤醒。如果使用Object.wait(),为了正确地执行(即确保等待足够的时间),您需要使用一个循环并查询经过的时间(例如System.currentTimeMillis()),以确保您等待足够长的时间。

从技术上讲,您可以使用Object.wait()实现与Thread.sleep()相同的功能,但是您需要编写更多的代码来正确执行此操作。

这篇文章也是相关且有用的讨论。


这两个函数有不同的目的。你可以使用其中一个来模拟另一个,但这将是棘手的,并且无疑比已提供的实现差。 - David Schwartz
是的,这很有道理。我忘记了虚假唤醒,Thread.sleep() 似乎不受此现象影响。 - wax

0
当一个线程调用sleep方法时,该线程将被添加到睡眠队列中。如果计算机时钟频率为100HZ,这意味着每10毫秒当前运行的进程将被中断。在保留线程的当前上下文之后,它将为每个线程减少值(-10ms)。当它降至零时,线程将移动到“等待CPU”队列中。当时间片来到这个线程时,它将再次运行。由于这个原因,它不会立即变成运行状态,所以实际休眠的时间比设置的值要长。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接