据说在 java 中采用 System.currentTimeMillis()
获取时间时,如果在高并发情况下很可能会出现性能问题。今天简单做了测试,简单做下总结,以便在以后的工作中使用的时候注意到该问题。
1. 测试代码如下
public static void main(String[] args) throws InterruptedException {
// 获取时间总次数
int threadCount = 100;
// 单线程循环获取
long singleStartTime = System.nanoTime();
for(int i=0; i<threadCount; i++) {
System.currentTimeMillis();
}
long singleDeltaTime = System.nanoTime() - singleStartTime;
System.out.println("单线程" + threadCount + "次耗时:" + singleDeltaTime + "ns");
// 使用线程池,多线程并发获取
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(Runtime.getRuntime().availableProcessors(), Runtime.getRuntime().availableProcessors() * 2,
60L, TimeUnit.SECONDS, new LinkedBlockingDeque<>(threadCount));
CountDownLatch subDownLatch = new CountDownLatch(threadCount);
CountDownLatch mainCountDownLatch = new CountDownLatch(1);
for(int i=0; i<threadCount; i++) {
threadPoolExecutor.execute(() -> {
try {
// 主线程先阻塞等待
mainCountDownLatch.await();
System.currentTimeMillis();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
subDownLatch.countDown();
}
});
}
long startTime = System.nanoTime();
// 主线程开始继续执行
mainCountDownLatch.countDown();
// 子线程等待线程池中其他活跃线程执行完
subDownLatch.await();
long deltaTime = System.nanoTime() - startTime;
System.out.println("并发" + threadCount + "次耗时:" + deltaTime + "ns");
}
2. 获取 100 次,单线程和多线程并发情况下的耗时对比
1)如果并发时不使用线程池,直接通过 new Thread() 方式执行结果如下, 可以看到并发情况下是单线程循环获取耗时的将近 40 倍。
![](https://img.haomeiwen.com/i13374653/0c5904c3b61162d6.jpeg)
2)如果使用线程池,就如上面代码中所示的,执行结果如下,可以看到是单线程循环获取耗时的 8 倍
![](https://img.haomeiwen.com/i13374653/4f11b15b31a7a388.jpeg)
结论:
1. 多线程并发调用 System.currentTimeMillis() 方法,比单线程循环调用性能差很多。
2. 使用线程池可以有效避免线程上下文切换引起的性能问题,平时推荐使用线程池。
2. 原因
由于System.currentTimeMillis() 是native 方法,没有深入挖掘,借用网上的说法,在执行该 native 方法时会涉及从 用户态到内核态的切换,去调用 Linux 内核的时钟源,时钟源只有一个,因此在大量并发时会造成严重的资源竞争,从而导致出现性能问题。
4. 优化
在高并发情况下,可以考虑通过后台线程定时去调用 System.currentTimeMillis() 方法获取时间,并将其保存在 java 变量中,在需要获取时间时,直接获取该变量的值即可。不过该方式可能会损失一定的时间精度,损失的精度取决于定时的时间间隔,间隔越大,精度损失越严重。
不过一般的项目中,并没有必要去做此优化,笔者做过验证,在 30w 并发之内,优化后的代码并没有比直接获取性能提高。在达到 3000w 并发时,也仅仅比直接过去快了 1s。不过还是有一些用。下面是优化的代码,可以酌情参考。
public class SystemClock {
private volatile long now;
private long period;
private SystemClock(long period) {
this.period = period;
// 先初始化为当前时间,不然第一次调用会有问题
this.now = System.currentTimeMillis();
// 后台线程任务开启定时刷新 now 的值
scheduleGetSystemTime();
}
// 对外方法
public static long currentTimeMillis() {
return Instance.INSTANCE.getCurrentMillis();
}
private void scheduleGetSystemTime() {
ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor(runnable -> {
Thread thread = new Thread(runnable, "system-lock-thread");
thread.setDaemon(true);
return thread;
});
scheduledExecutorService.scheduleAtFixedRate(() -> now = System.currentTimeMillis(), 0, period, TimeUnit.MILLISECONDS);
}
private static class Instance {
private final static SystemClock INSTANCE = new SystemClock(1);
}
private long getCurrentMillis() {
return now;
}
}
网友评论