抛弃性能不佳的System.currentTimeMillis(),手撸一个低开销获取时间戳工具

2021年09月15日 阅读数:3
这篇文章主要向大家介绍抛弃性能不佳的System.currentTimeMillis(),手撸一个低开销获取时间戳工具,主要内容包括基础应用、实用技巧、原理机制等方面,希望对大家有所帮助。

你知道的越多,不知道的就越多,业余的像一棵小草!

你来,咱们一块儿精进!你不来,我和你的竞争对手一块儿精进!
html

编辑:业余草

推荐:https://www.xttblog.com/?p=5277

你们好,我是业余草,这是个人第 447 篇原创!java

你或许据说过,在 Java 中调用 System.currentTimeMillis() 会有一些性能开销,在某些场景下,System.nanoTime() 更具优点!linux

好比,测试方法的耗时时间:git

public void save(){
    long start = System.currentTimeMillis();
    // doSomething() ...
    System.out.println(System.currentTimeMillis() - start);
}

这里建议你System.currentTimeMillis()改成System.nanoTime()github

public void save(){
    long start = System.nanoTime();
    // doSomething() ...
    System.out.println(System.nanoTime() - start);
}

缘由咱们下面慢慢展开。web

昨天群里还有人说,可使用 StopWatch。岂不知,StopWatch 背后也是System.currentTimeMillis()缓存

public void save(){
    StopWatch stopWatch = new StopWatch();
    stopWatch.start();
    // doSomething() ...
    stopWatch.stop();
    System.out.println(stopWatch.prettyPrint());
}
StopWatch底层时间获取

System.currentTimeMillis() 的缺点

System.currentTimeMillis()返回的是毫秒数,System.nanoTime()返回的是纳秒数。若是方法跑的比较快,毫秒的测试就更不许确了。安全

1000 皮秒 = 1纳秒 
1000000 皮秒 = 1微秒
1000000000 皮秒 = 1毫秒
1000000000000 皮秒 = 1秒

1s = 1000 ms 毫秒
1ms = 1000000 ns 纳秒

更况且,currentTimeMillis依赖底层操做系统,nanoTime则是有 JVM 维护。性能优化

展开来讲就是,咱们在 Java 中获取时间戳的方法是System.currentTimeMillis()返回的是毫秒级的时间戳。查看源码注释,写的比较清楚,虽然该方法返回的是毫秒级的时间戳,但精度取决于操做系统,不少操做系统返回的精度是 10 毫秒。微信

/**
* Returns the current time in milliseconds.  Note that
* while the unit of time of the return value is a millisecond,
* the granularity of the value depends on the underlying
* operating system and may be larger.  For example, many
* operating systems measure time in units of tens of
* milliseconds.
*
* <p> See the description of the class <code>Date</code> for
* a discussion of slight discrepancies that may arise between
* "computer time" and coordinated universal time (UTC).
*
@return  the difference, measured in milliseconds, between
*          the current time and midnight, January 1, 1970 UTC.
@see     java.util.Date
*/

public static native long currentTimeMillis();

以 HotSpot 源码为例,源码在 hotspot/src/os/linux/vm/os_linux.cpp 文件中,有一个javaTimeMillis()方法,这就是System.currentTimeMillis()的 native 实现。

jlong os::javaTimeMillis() {
  timeval time;
  int status = gettimeofday(&time, NULL);
  assert(status != -1"linux error");
  return jlong(time.tv_sec) * 1000  +  jlong(time.tv_usec / 1000);
}

这是 C++ 写的,我也看不懂。咱们直接拿老外的研究来学习:http://pzemtsov.github.io/2017/07/23/the-slow-currenttimemillis.html

总结起来缘由是System.currentTimeMillis调用了gettimeofday()

  • 调用 gettimeofday()须要从用户态切换到内核态;
  • gettimeofday()的表现受 Linux系统的计时器(时钟源)影响,在 HPET 计时器下性能尤为差;
  • 系统只有一个全局时钟源,高并发或频繁访问会形成严重的争用。

咱们测试一下System.currentTimeMillis()在不一样线程下的性能,这里使用中间件经常使用的JHM来测试,测试 1 到 128 线程下获取 1000 万次时间戳须要的时间分别是多少,这里给出在个人电脑上的测试数据:

System.currentTimeMillis()性能测试

还有一个问题就是,currentTimeMillis 获取的是系统时间源。所以,系统时间变动,或者系统自动进行了时间同步,计算两次获取的差值,多是负数。

另外System.currentTimeMillis()返回自纪元(即自 1970 年 1 月 1 日 UTC 午夜以来的毫秒数)。若是你的系统设置的时间小于这个时间,那么 currentTimeMillis 的取值也多是负数。固然几乎没人会这么设置时间,除非是黑客。

设置系统时间

小总结:使用System.currentTimeMillis()要注意精度、性能开销、时间同步影响准确性、时间不安全多是负数、高并发场景随机数不均衡等问题。

System.nanoTime() 的缺点

System.nanoTime()是 JDK 1.5 才推出的,所以 1.5 以前的办法没法使用。

第二,源码注释中描述它是安全的。但在老外的使用过程当中发现,它有时候也不安全,返回的也多是负数。

另外官方建议,可使用它来测量 elapsed time,不能用来看成 wall-clock time 或 system time。

This method can only be used to measure elapsed time and is not related to any other notion of system or wall-clock time.

网上还暴露出,多核处理器不一样核心的启动时间可能不彻底一致,这样可能会形成System.nanoTime()计时错误。参考:https://stackoverflow.com/questions/510462/is-system-nanotime-completely-useless

手撸一个 currentTimeMillis

先定义一个工具类:TimeUtil。

/**
 * 弱精度的计时器,考虑性能不使用同步策略。
 */

public class TimeUtil {
    private static long CURRENT_TIME = System.currentTimeMillis();

    public static final long currentTimeMillis() {
        return CURRENT_TIME;
    }

    public static final void update() {
        CURRENT_TIME = System.currentTimeMillis();
    }

}

而后起一个定时器,定时更新维护时间。

import java.util.Timer;
import java.util.TimerTask;

public class TimerServer {
    private static final TimerServer INSTANCE = new TimerServer();
    private final Timer timer;
    
    private TimerServer(){
        timer = new Timer("业余草Timer"true);
        timer.schedule(updateTime(), 0L20L);
    }

    // 系统时间定时更新任务
    private TimerTask updateTime() {
        return new TimerTask() {
            @Override
            public void run() {
                TimeUtil.update();
            }
        };
    }

    public static final TimerServer getInstance() {
        return INSTANCE;
    }
}

或者直接用一个 TimeUtil 类搞定。

public final class TimeUtil {

    private static volatile long currentTimeMillis;

    static {
        currentTimeMillis = System.currentTimeMillis();
        Thread daemon = new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    currentTimeMillis = System.currentTimeMillis();
                    try {
                        TimeUnit.MILLISECONDS.sleep(1);
                    } catch (Throwable e) {

                    }
                }
            }
        });
        daemon.setDaemon(true);
        daemon.setName("业余草-time-tick-thread");
        daemon.start();
    }

    public static long currentTimeMillis() {
        return currentTimeMillis;
    }
}

这样作的好处就是,在高并发场景下,对时间要求较高的场景,则能够本身维护系统时钟。

通过 JMH 测试对比(测试代码能够加我微信:codedq,免费获取),咱们手撸的 TimeUtil 在 1-128 线程下的性能表现很是强劲,比系统自带的System.currentTimeMillis()高出近 876 倍。

低开销获取时间戳

好比:阿里的 Sentinel,Cobar等。Twitter 的 Snowflake(不少人在实现 Snowflake 时,采用了 System.currentTimeMillis())。

总结

虽然缓存时间戳性能能提高不少,但这也仅限于很是高的并发系统中,通常比较适用于高并发的中间件,若是通常的系统来作这个优化,效果并不明显。性能优化仍是要抓住主要矛盾,解决瓶颈,切忌不可过分优化。

参考资料

  • https://en.wikipedia.org/wiki/High_Precision_Event_Timer
  • https://en.wikipedia.org/wiki/Time_Stamp_Counter
  • http://pzemtsov.github.io/2017/07/23/the-slow-currenttimemillis.html
  • https://stackoverflow.com/questions/510462/is-system-nanotime-completely-useless

本文分享自微信公众号 - 业余草(yyucao)。
若有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一块儿分享。