jdk 中的 wait() sleep() 竟然还有个带纳秒的参数, 请问你是来逗我的么?
一次偶然翻 jdk 代码,发现 jdk 中的 wait() , sleep() 方法,竟然还有个支持纳秒的参数,我一想 1 纳秒光也只能跑3米,现代硬件真能支持纳秒级的定时任务么? 不过一看实现,发现是 jdk 在跟我们开玩笑呢,今天我们就来聊聊JVM 中的时间精度.
好了,我们先来看看这两个逗比方法的实现
public final void wait(long timeout, int nanos) throws InterruptedException {
if (timeout < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
if (nanos < 0 || nanos > 999999) {
throw new IllegalArgumentException(
"nanosecond timeout value out of range");
}
if (nanos > 0) {
timeout++;
}
wait(timeout);
}
这个方法有两个参数:一个毫秒,一个是在毫秒基础上的纳秒数 , 例如135毫秒200001纳秒,他的实现只是判断了下纳秒数是否大于0,如果大于0就把毫秒加1.
下面是sleep()方法
```
public static void sleep(long millis, int nanos)
throws InterruptedException {
if (millis < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
if (nanos < 0 || nanos > 999999) {
throw new IllegalArgumentException(
"nanosecond timeout value out of range");
}
if (nanos >= 500000 || (nanos != 0 && millis == 0)) {
millis++;
}
sleep(millis);
}
```
这里唯一不同的是,判断了纳秒数是否大于 500000 ,来控制毫秒数是否加1, 50万等于0.5毫秒,相当于四舍五入了.
虽然jdk里这两个实现都没有真正把等待时间精确到纳秒,但是我们仍然发现一个细节就是wait() 和 sleep() 实现不一致,我们姑且可以这样猜测,wait()
方法执行的时候,需要将自己加入到 monitorobject 的等待队列,并且释放掉所持有的锁,从而会消耗0.5毫秒左右的时间,而 sleep 并不需要这些操作.
另外一个区别是 wait(0) 相当于一直等待直到被唤醒,而 sleep(0) 相当于释放 CPU 执行权限给其他优先级更高的线程,状态从运行中转换成就绪而不是阻塞.
定时任务的实现
定时任务是现代软件不可缺少的一个功能,例如邮件服务器每隔 5 秒去查询一下是否有新邮件到达,注册中心每隔一段时间发一个心跳包去检测服务是否可达,基金系统每天早上固定时间开始跑日切任务来给投资人计算收益,等等. 那么 JVM 里是如何实现定时任务的? 我们平时的定时任务都是按秒为精度的,例如 00:00:01 开始跑日切,每隔 5 秒去查询一下服务状态,那么我们能否每隔 5 纳秒执行一次任务呢?
Java 中我们要实现定时任务,有两种方式,一种通过 timer 类, 另外一种是 JUC 中的 ScheduledExecutorService 他们的内部实现并不复杂,基本上都是借助一个 DelayedWorkQueue 延迟队列,该队列用一个数组存储被调度的任务,根据待调度时间的长短进行排序,越早越靠前,调度线程每次取第一个任务, 然后阻塞这个任务的延迟执行的时间.
public RunnableScheduledFuture<?> take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
for (;;) {
RunnableScheduledFuture<?> first = queue[0];
if (first == null)
available.await();
else {
long delay = first.getDelay(NANOSECONDS);
if (delay <= 0)
return finishPoll(first);
first = null; // don't retain ref while waiting
if (leader != null)
available.await();
else {
Thread thisThread = Thread.currentThread();
leader = thisThread;
try {
available.awaitNanos(delay);
} finally {
if (leader == thisThread)
leader = null;
}
}
}
}
} finally {
if (leader == null && queue[0] != null)
available.signal();
lock.unlock();
}
}
time 类采用的 object.wait() 方法阻塞 , 而 ScheduledExecutorService 采用的 JUC 中的 Condition.await() 进行阻塞 ,而 Condition 是借助 AQS 实现的,最终调用的 LockSupport.park(long nans) .
LockSupport.park(long nans) 会让线程挂起,并且通过反射的方式,在thread 对象的 blocker 设置是谁阻塞了这个线程.这里调用了 UNSAFE.park(false, nanos); 表示采用相对时间来计时.
// 反射的实现,通过 blocker 属性 相对于 thread 对象的初始地址的偏移量 设置属性的值
private static void setBlocker(Thread t, Object arg) {
// Even though volatile, hotspot doesn't need a write barrier here.
UNSAFE.putObject(t, parkBlockerOffset, arg);
}
public static void parkNanos(Object blocker, long nanos) {
if (nanos > 0) {
Thread t = Thread.currentThread();
setBlocker(t, blocker);
UNSAFE.park(false, nanos);
setBlocker(t, null);
}
}
好了,在这里我们可以断定一个事实就是,基于jdk的程序暂时无法支持纳秒级别的定时任务,因为其他定时任务的框架如 spring 和 quartz 都是基于此实现, 尽管现实中可能也没有这种需求.
linux 中的定时器
到这里文章并没有结束,我们想借此科普下 linux 中的定时器是怎么实现的.
现代的硬件计时器都是通过晶体振动产生的方波信号输入来完成时钟信号同步的,CPU中有一个可编程间隔定时器 PIT ,该计数器可以通过编程的方式由程序设置一个初始值,每过一个时钟周期,该初始值会减1,当该初始值被减到0时,就通过导线向 CPU 发送一个时钟中断, CPU 每次执行完一个指令后,就会检查中断寄存器中是否有中断信号,如果有就取出来执行对应的中断回调程序,也就是对应的定时器任务,当然操作系统也会维护一个延迟队列,一个一个的去设置PIT计时器的初始值.
linux 中以铯133的振荡频率来定义秒,而人类更偏向于根据地球自转和公转来定义秒,但是地球自转并不是一个恒定的值,而前者可以保证是一个恒定的值,所以 linux 内核采用第一种方式计算秒,而目前地球的自转一直在变慢,所以会有闰秒的概念,这样就需要NTP协议进行时钟同步,所以我们通过当前时间生成订单号的时候可能会重复.
java 中的时间精度
System.currentTimeMillis() 的精度是毫秒级,在windows平台,该精度可能大于 10ms
System.nanoTime() 是纳秒级别,精度在 100ns 左右,所以如果要获取更精准的时间建议用后者, Random 类为了防止冲突就用nanoTime生成种子.

人生真是寂寞如雪啊~
网友评论