一、问题背景
最近在网上看到有说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()
这样调用一下就可以了。这里简单看下这个代码设计逻辑。
首先,可以看到这个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
被类加载后,就创建了此调度器线程池。
四、总结
通过这种异步定时更新时间戳变量的方式去获取时间戳,其实还是有一定的精度丢失的,毕竟定时去调度是会造成时间差的,及时是以很小的时间间隔去调度。但是在大多数场景下,这点误差是可以接受的。故而通过这种方式,优化获取时间的性能还是可取的。