一直流传着Timer使用的是绝对时间,ScheduledThreadPoolExecutor使用的是相对时间,那么ScheduledThreadPoolExecutor是如何实现相对时间的?



先看看ScheduledThreadPoolExecutor中实现定时调度的模型,很简单,内部用了无界的DelayQueue作为线程池的队列,而DelayQueue的内部又使用的是一个PriorityQueue,那么,最先需要定时调度的任务位于队首。定时任务实现逻辑大概如此:创建ScheduledThreadPoolExecutor对象的时候会记录一个常量值t,定时任务中有一个以t为基础的多久以后会被执行的属性,在线程拿到队首任务(可能等待了一段时间)执行后,会修改这个属性为下一次要执行的基于t的时间量,然后将其再放入队列中。整个逻辑都在任务的run方法中:

public

void

run() {
    if

(isPeriodic())
        runPeriodic();
    else
        ScheduledFutureTask.super.run();
}

如果是周期性任务,会执行runPeriodic:

private

void

runPeriodic() {
    boolean

ok = ScheduledFutureTask.
super.runAndReset();
    boolean

down = isShutdown();
    //
Reschedule if not cancelled and not shutdown or policy allows
    if

(ok && (!down ||
               (getContinueExistingPeriodicTasksAfterShutdownPolicy()
&&
                !isTerminating())))
{
        long

p = period;
        if

(p >
0)
            time
+= p;
        else
            time
= triggerTime(-p);
        ScheduledThreadPoolExecutor.super.getQueue().add(this);
    }
    //
This might have been the final executed delayed
    //
task.  Wake up threads to check.
    else

if

(down)
        interruptIdleWorkers();
}

下面这段代码就是周期性任务实现的逻辑:

if

(p >
0)
    time
+= p;
else
    time
= triggerTime(-p);
ScheduledThreadPoolExecutor.super.getQueue().add(this);

重新回到相对时间问题,首先看看DelayQueue的take方法:

public

E take()
throws

InterruptedException {
    final

ReentrantLock lock =
this.lock;
    lock.lockInterruptibly();
    try

{
        for

(;;) {
            E
first = q.peek();
            if

(first ==
null)
{
                available.await();
            }
else

{
                long

delay =  first.getDelay(TimeUnit.NANOSECONDS);
                if

(delay >
0)
{
                    long

tl = available.awaitNanos(delay);
                }
else

{
                    E
x = q.poll();
                    assert

x !=
null;
                    if

(q.size() !=
0)
                        available.signalAll();
//
wake up other takers
                    return

x;
 
                }
            }
        }
    }
finally

{
        lock.unlock();
    }
}

拿到队列里的第一个元素(也就是最先需要执行的),但并不删除,获取该元素的等待时间(也就是getDelay),然后不断的awaitNanos。如果此时加入了一个比这个队首元素还要先执行的任务会怎样?看下add和offer方法的实现(add方法就是直接调用的offer):

public

boolean

offer(E o) {
    final

ReentrantLock lock =
this.lock;
    lock.lock();
    try

{
        E
first = q.peek();
        q.offer(o);
        if

(first ==
null

|| o.compareTo(first) <
0)
            available.signalAll();
        return

true
;
    }
finally

{
        lock.unlock();
    }
}

变量q即是一个PriorityQueue,如果o对象表示的任务比目前队首的任务更先执行,那么q.offer(o)会将o弄到队首,在这种情况下o.compareTo(first)是小于0的,因此会通知在available上等待的线程。而在take方法里,线程一直在available上awaitNanos,此时若被唤醒,它就会继续循环,重新拿到队列的第一个元素,也就是新加入的元素并重复之前的过程,这样,最先需要调度的任务就永远排在第一位。

回到take方法的long delay = first.getDelay(TimeUnit.NANOSECONDS)这一句,在ScheduledThreadPoolExecutor实现中,这个first变量的类型就是ScheduledThreadPoolExecutor.ScheduledFutureTask,这是ScheduledThreadPoolExecutor中的一个私有类,看看其getDelay方法的实现:

public

long

getDelay(TimeUnit unit) {
    return 

unit.convert(time - now(), TimeUnit.NANOSECONDS);
}

其中的now方法:

final

long

now() {
    return

System.nanoTime() - NANO_ORIGIN;
}

刚开始看到这个代码时,心想这何以实现相对时间。潜意识中对nanoTime认识一直是这样的:返回值是纳秒,与currentTimeMillis()一样返回的是与协调世界时 1970 年 1 月 1 日午夜之间的时间差。经过测试与查看API文档,原来对nanoTime()的认识一直是错误的。

API中关于nanoTime是这么描述的:

返回最准确的可用系统计时器的当前值,以毫微秒为单位。
此方法只能用于测量已过的时间,与系统或钟表时间的其他任何时间概念无关。返回值表示从某一固定但任意的时间算起的毫微秒数(或许从以后算起,所以该值可能为负)。此方法提供毫微秒的精度,但不是必要的毫微秒的准确度。它对于值的更改频率没有作出保证。在取值范围大于约 292 年(263 毫微秒)的连续调用的不同点在于:由于数字溢出,将无法准确计算已过的时间。

返回值的单位为毫微秒不是该方法的重点,重点在于“与系统或钟表时间的其他任何时间概念无关。返回值表示从某一固定但任意的时间算起的毫微秒数”,也就是说,它的返回值是一个相对“某一固定但任意的时间”的偏移量,而不依赖于系统时钟是否改变,也无法通过这个方法的返回值计算当前日期。而这个相对性正是ScheduledThreadPoolExecutor所需要的。

很多计算代码运行耗时的地方使用了currentTimeMillis(),那么在系统时间变动的那一刻(如NTP时间同步),耗时计算结果是不准确的,尤其是时间变动较大时,如果在日志中发现某个调用突然耗时很大,还以为出现什么问题了。

关于nanoTime有些有趣的问题,用该方法计算运行耗时得出的结果竟然会是负数:http://stackoverflow.com/questions/510462/is-system-nanotime-completely-useless,只是看到这个话题,我的系统上(xp
sp3)没有重现。

最后,题外话,关于系统时间:尽可能不要在生产代码中使用Thread#sleep,因为“此操作受到系统计时器和调度程序精度和准确性的影响”。

ScheduledThreadPoolExecutor与System#nanoTime的更多相关文章

  1. System.nanoTime

    System.currentTimeMillis()返回的毫秒,这个毫秒其实就是自1970年1月1日0时起的毫秒数. System.nanoTime()返回的是纳秒,nanoTime而返回的可能是任意 ...

  2. System.nanoTime与System.currentTimeMillis的理解与区别

    System类代表系统,系统级的很多属性和控制方法都放置在该类的内部.该类位于java.lang包. 平时产生随机数时我们经常拿时间做种子,比如用System.currentTimeMillis的结果 ...

  3. System.nanoTime与System.currentTimeMillis的区别

    平时产生随机数时我们经常拿时间做种子,比如用 System.currentTimeMillis的结果,但是在执行一些循环中使用了System.currentTimeMillis,那么每次的结 果将会差 ...

  4. System.nanoTime()的使用

    纳秒 ns(nanosecond):纳秒, 时间单位.一秒的10亿分之一,即等于10的负9次方秒.常用作 内存读写速度的单位. 1纳秒=0.000001 毫秒 1纳秒=0.00000 0001秒 ja ...

  5. System.nanoTime理解

    JDK1.5之后java中的计时给出了更精确的方法:System.nanoTime(),输出的精度是纳秒级别,这个给一些性能测试提供了更准确的参考. 但是这个方法有个需要注意的地方,不能用来计算今天是 ...

  6. 【填坑纪事】一次用System.nanoTime()填坑System.currentTimeMills()的实例记录

    JDK提供了两个方法,System.currentTimeMillis()和System.nanoTime(),这两个方法都可以用来获取表征当前时间的数值.但是如果不仔细辨别这两个方法的差别和联系,在 ...

  7. System.nanoTime与System.currentTimeMillis的区别(转)

    原文地址:http://blog.csdn.net/dliyuedong/article/details/8806868 平时产生随机数时我们经常拿时间做种子,比如用System.currentTim ...

  8. 对于应用需要记录某个方法耗时的场景,必须使用clock_gettime传入CLOCK_MONOTONIC参数,该参数获得的是自系统开机起单调递增的纳秒级别精度时钟,相比gettimeofday精度提高不少,并且不受NTP等外部服务影响,能准确更准确来统计耗时(java中对应的是System.nanoTime),也就是说所有使用gettimeofday来统计耗时(java中是System.curre

    对于应用需要记录某个方法耗时的场景,必须使用clock_gettime传入CLOCK_MONOTONIC参数,该参数获得的是自系统开机起单调递增的纳秒级别精度时钟,相比gettimeofday精度提高 ...

  9. 我的Java开发学习之旅------>System.nanoTime与System.currentTimeMillis的区别

    首先来看一道题:下面代码的输出结果是什么? import java.util.HashMap; import java.util.Map; public class HashMapTest { pub ...

  10. java: new Date().getTime() 与 System.currentTimeMillis() 与 System.nanoTime()

    java使用new Date()和System.currentTimeMillis()获取当前时间戳   在开发过程中,通常很多人都习惯使用new Date()来获取当前时间,使用起来也比较方便,同时 ...

随机推荐

  1. Kubernetes-10:Ingress-nginx介绍及演示

    Ingress介绍 Ingress是什么? ingress 是除了 hostport  nodeport  clusterIP以及云环境专有的负载均衡器外的访问方式,官方提供了Nginx ingres ...

  2. 小程序云开发 Collection.watch 监听器构建和销毁

    小程序云开发 Collection.watch 监听器构建和销毁 构建和销毁代码示例 // release/chatroom/index.js const db = wx.cloud.database ...

  3. CCIA数安委等组织发起“个人信息保护影响评估专题工作”,合合信息首批入选试点

    近期,"个人信息保护影响评估专题工作"(简称"PIA专题工作")试点评估结果正式发布.PIA专题工作组由中国电子技术标准化研究院.中国信息通信院等单位的法律与技 ...

  4. vue3项目部署到Github

    此教程适应于以webpack,vue-cli,vite等脚手架构建的vue项目.当然,vue2和vue3都是可以滴. 1. 前提:你的代码库已经提交到Github上 如果没有的话,请到GitHub上新 ...

  5. 全网最适合入门的面向对象编程教程:49 Python函数方法与接口-函数与方法的区别和lamda匿名函数

    全网最适合入门的面向对象编程教程:49 Python 函数方法与接口-函数与方法的区别和 lamda 匿名函数 摘要: 在 Python 中,函数和方法都是代码的基本单元,用于封装和执行特定的任务:它 ...

  6. WiFi基础(四):WiFi工作原理及WiFi接入过程

    liwen01 2024.09.16 前言 802.11 无线 WiFi 网有三类帧:数据帧.管理帧.控制帧.与有线网相比,无线 WiFi 网会复杂很多.大部分应用软件开发对 WiFi 的控制帧和管理 ...

  7. t-io 学习笔记(一)

        基础介绍理解篇 序:本文也是在t-io官网学习的基础上写的理解学习笔记:1.什么是t-io? t-io是基于JVM的网络编程框架,和netty属同类,所以netty能做的t-io都能做,考虑到 ...

  8. [OI] 模拟退火

    模拟退火是一种适合求样本点较大的多峰函数极值的方法. 模拟退火有几个参数:初始温度(\(T_{0}\)),终止温度(\(T_{e}\))和降温参数 \(d\),具体地,模拟退火是让每次的当前温度 \( ...

  9. Dockerfile相关(推送镜像?私有仓库?)(九)

    上面我们讲到了 Dockerfile 的基本写法以及构建镜像的时候一些注意事项,那么镜像构建完成后,如何把我们的镜像给到别人使用呢?第一种方法就是利用 Docker 官方提供的公共的 Docker H ...

  10. .NET 8.0 酒店管理系统设计与实现

    前言 给大家推荐一个基于.NET 8.0 的中小型酒店设计的管理系统. 随着酒店的日常工作增加,很难用人工去进行处理一些繁琐的数据,也可能会因为人工的失误而造成酒店的损失,因此需要一款可以协助酒店进行 ...