抛弃性能不佳的System.currentTimeMillis(),手撸一个低开销获取时间戳工具
共 9366字,需浏览 19分钟
·
2021-09-12 11:52
你知道的越多,不知道的就越多,业余的像一棵小草!
你来,我们一起精进!你不来,我和你的竞争对手一起精进!
编辑:业余草
推荐:https://www.xttblog.com/?p=5277
大家好,我是业余草,这是我的第 447 篇原创!
你或许听说过,在 Java 中调用 System.currentTimeMillis() 会有一些性能开销,在某些场景下,System.nanoTime() 更具优势!
比如,测试方法的耗时时间:
public void save(){
long start = System.currentTimeMillis();
// doSomething() ...
System.out.println(System.currentTimeMillis() - start);
}
这里建议你System.currentTimeMillis()
改为System.nanoTime()
。
public void save(){
long start = System.nanoTime();
// doSomething() ...
System.out.println(System.nanoTime() - start);
}
原因我们下面慢慢展开。
昨天群里还有人说,可以使用 StopWatch。岂不知,StopWatch 背后也是System.currentTimeMillis()
。
public void save(){
StopWatch stopWatch = new StopWatch();
stopWatch.start();
// doSomething() ...
stopWatch.stop();
System.out.println(stopWatch.prettyPrint());
}
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 万次时间戳需要的时间分别是多少,这里给出在我的电脑上的测试数据:
还有一个问题就是,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(), 0L, 20L);
}
// 系统时间定时更新任务
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