高并发下System.currentTimeMillis()性能问题及优化方案
文章目录
- 背景
- System.currentTimeMillis()
- 性能测试
- 单线程测试
- 多线程测试
- 原因
- 优化
- 优化代码
- 单线程测试
- 多线程测试
- 参考
背景
最近在看asyncTool
源码发现了System.currentTimeMillis
存在卡顿问题,所以就详细研究了下。具体如何呢?我们来看看
System.currentTimeMillis()
jdk版本jdk11
可以看到该方法被@HotSpotIntrinsicCandidate
注解修饰,代表使用HotSpot的实现代替JDK源码的实现方式,即基于CPU指令集。
方法的注释也说的很清楚
以毫秒为单位返回当前时间。 请注意,虽然返回值的时间单位是毫秒,但值的粒度取决于底层操作系统,并且可能更大。 例如,许多操作系统以几十毫秒为单位测量时间。
有关“计算机时间”和协调世界时 (UTC) 之间可能出现的细微差异的讨论,请参阅类 Date 的描述。
回报:
当前时间与 UTC 1970 年 1 月 1 日午夜之间的差异,以毫秒为单位。
说明该方法存在时间误差,有精度问题,大概误差在几十毫秒内,因操作系统而异
性能测试
测试机器:
操作系统:
- macOS
- 版本:12.3.1
- 芯片: Apple M1
- CPU核数:8核
单线程测试
- 测试代码
@Testpublic void testSingleThread() {//测试一百次循环,每次循环调用1千万System.currentTimeMillis()次数for (int t = 0; t < 100; t++) {StopWatch stopWatch = new StopWatch();stopWatch.start();//获取一千万次时间for (int i = 0; i < 10000000; i++) {System.currentTimeMillis();}stopWatch.stop();long totalTimeMillis = stopWatch.getTotalTimeMillis();System.out.println(totalTimeMillis);}}
- 测试结果
其实可以看到消耗时间大概在134毫秒左右,但是最大值在198毫秒的,误差范围竟然高达50ms
.
多线程测试
- 测试代码
@Testpublic void multiThread() throws Exception{// 测试执行1次StopWatch stopWatch = new StopWatch();stopWatch.start();System.currentTimeMillis();stopWatch.stop();long totalTimeNanos = stopWatch.getLastTaskTimeNanos();System.out.println(totalTimeNanos);System.out.println("=====================");//100个线程各执行一次CountDownLatch wait = new CountDownLatch(1);CountDownLatch threadLatch = new CountDownLatch(100);for (int i = 0; i < 100; i++) {new Thread(() -> {try {StopWatch watch = new StopWatch();//先阻塞住所有线程wait.await();watch.start();System.currentTimeMillis();watch.stop();System.out.println(watch.getTotalTimeNanos());} catch (InterruptedException e) {e.printStackTrace();} finally {threadLatch.countDown();}}).start();}//暂停1s保证线程创建完成TimeUnit.SECONDS.sleep(1);wait.countDown();threadLatch.await();}
- 测试结果
可以看到测试结果平均在500ms左右,但是极端个别数据到达了11667ms,有点夸张
原因
单线程下产生延迟说明在系统底层上该线程和其他进程或线程产生了竞争,探究下hotspot中的实现:
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);
}
以下是查询得知,涉及到汇编层面了。
- 调用
gettimeofday()
需要从用户态切换到内核态; gettimeofday()
的表现受系统的计时器(时钟源)影响,在HPET计时器下性能尤其差;- 系统只有一个全局时钟源,高并发或频繁访问会造成严重的争用。
优化
优化方式很简单,如果我们的误差允许在1ms内,那我们保证在1ms内只调用一次System.currentTimeMillis()
,在1ms内的其他调用都直接使用这次调用的结果这样就大大避免了和其他线程抢夺资源的概率。也减少了线程上下文的切换,以及用户态到内核态的切换
优化代码
优化新增工具类SystemClock
/*** @author : wh* @date : 2022/4/26 22:42* @description:*/
public class SystemClock {private final int period;private final AtomicLong now;private static final String THREAD_NAME ="system.clock";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() {ScheduledThreadPoolExecutor scheduler = new ScheduledThreadPoolExecutor(1, r -> {Thread thread = new Thread(r, THREAD_NAME);thread.setDaemon(true);return thread;});scheduler.scheduleAtFixedRate(() -> now.set(System.currentTimeMillis()), period, period, TimeUnit.MILLISECONDS);}private long currentTimeMillis() {return now.get();}/*** 用来替换原来的System.currentTimeMillis()*/public static long now() {return instance().currentTimeMillis();}
}
单线程测试
- 测试代码
@Testpublic void testSingleThreadBySystemClock() {//测试一百次循环,每次循环调用1千万System.currentTimeMillis()次数for (int t = 0; t < 100; t++) {StopWatch stopWatch = new StopWatch();stopWatch.start();//获取一千万次时间for (int i = 0; i < 10000000; i++) {// 使用优化的代码SystemClock.now();}stopWatch.stop();long totalTimeMillis = stopWatch.getTotalTimeMillis();System.out.println(totalTimeMillis);}}
- 运行结果
可以看到性能差距非常明显,都在5ms左右,相差了20多倍的效率
多线程测试
@Testpublic void multiThreadBySystemClock() throws Exception{// 测试执行1次StopWatch stopWatch = new StopWatch();stopWatch.start();SystemClock.now();stopWatch.stop();long totalTimeNanos = stopWatch.getLastTaskTimeNanos();System.out.println(totalTimeNanos);System.out.println("=====================");//100个线程各执行一次CountDownLatch wait = new CountDownLatch(1);CountDownLatch threadLatch = new CountDownLatch(100);for (int i = 0; i < 100; i++) {new Thread(() -> {try {StopWatch watch = new StopWatch();//先阻塞住所有线程wait.await();watch.start();SystemClock.now();watch.stop();System.out.println(watch.getTotalTimeNanos());} catch (InterruptedException e) {e.printStackTrace();} finally {threadLatch.countDown();}}).start();}//暂停1s保证线程创建完成TimeUnit.SECONDS.sleep(1);wait.countDown();threadLatch.await();}
整体都非常稳定,没有太大波动
参考
博客