优化高并发下获取系统当前时间毫秒数性能问题

一、问题背景

最近在网上看到有说Java当中根据System.currentTimeMillis()获取当前毫秒时间戳的方式在高并发下会有性能问题。这里不再去验证这个问题是否存在了,网上很多参考可以证明这个问题确实存在。

这里记录一个解决此问题的工具代码,原理是利用了一个定时任务去异步获取时间写入到原子长整型中,然后获取时间就可以直接从此字段上获取了。

二、源代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45

import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;

public class SystemClock {

private final int period;

private final AtomicLong now;

private static class InstanceHolder {
private static final SystemClock INSTANCE = new SystemClock(1);
}

private SystemClock(int period) {
this.period = period;
this.now = new AtomicLong(System.currentTimeMillis());
scheduleClockUpdating();
}

private static SystemClock instance() {
return InstanceHolder.INSTANCE;
}

private void scheduleClockUpdating() {
ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(runnable -> {
Thread thread = new Thread(runnable, "System Clock");
thread.setDaemon(true);
return thread;
});
scheduler.scheduleAtFixedRate(() -> now.set(System.currentTimeMillis()), period, period, TimeUnit.MILLISECONDS);
}

private long currentTimeMillis() {
return now.get();
}

public static long now() {
return instance().currentTimeMillis();
}
}

三、逻辑分析

可以看到这个SystemClock类只有一个静态public方法now()。所以要获取当前时间只需要SystemClock.now()这样调用一下就可以了。这里简单看下这个代码设计逻辑。

image-20210419091748140

首先,可以看到这个SystemClock依赖了内部的一个InstanceHolder去获取一个单例的SystemClock对象。这里运用了私有静态类的方式做到懒汉式线程安全的单例模式, 可以完全不使用同步关键字, 完全利用JVM的机制去保证了线程安全,可以学习下这种单例模式的写法.

然后具体看SystemClock中的实现,我们可以看到通过SystemClock.now()获取的时间,实际上是来自于SystemClock中的私有成员变量now,这是一个AtomicLong的类型。既然知道我们的时间戳是从这个字段上获取的,那么我们就只需要顺藤摸瓜,看下这个成员变量是什么时候被赋值的就行了。

于是,我们看到scheduleClockUpdating这个方法:

1
2
3
4
5
6
7
8
private void scheduleClockUpdating() {
ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(runnable -> {
Thread thread = new Thread(runnable, "System Clock");
thread.setDaemon(true);
return thread;
});
scheduler.scheduleAtFixedRate(() -> now.set(System.currentTimeMillis()), period, period, TimeUnit.MILLISECONDS);
}

可以看到,这里实际上就是开启了一个ScheduledExecutorService线程池,来定时调用System.currentTimeMillis(),来将时间异步更新到成员变量now当中。那么这个scheduler是什么时候被创建的呢,可以看到scheduleClockUpdating()方法的调用点:

1
2
3
4
5
private SystemClock(int period) {
this.period = period;
this.now = new AtomicLong(System.currentTimeMillis());
scheduleClockUpdating();
}

所以可以知道,scheduler是在SystemClock构造器中被创建的,又由于SystemClock是私有构造器,而其是InstanceHolder中作为静态成员变量存在的,故实际上是当InstanceHolder被类加载后,就创建了此调度器线程池。

四、总结

通过这种异步定时更新时间戳变量的方式去获取时间戳,其实还是有一定的精度丢失的,毕竟定时去调度是会造成时间差的,及时是以很小的时间间隔去调度。但是在大多数场景下,这点误差是可以接受的。故而通过这种方式,优化获取时间的性能还是可取的。