System.currentTimeMillis的性能,真有如此不堪吗?

共 7918字,需浏览 16分钟

 ·

2021-07-27 08:12

# 疑惑,System.currentTimeMillis真有性能问题?


最近我在研究一款中间件的源代码时,发现它获取当前时间不是通过System.currentTimeMillis,而是通过自定义的System.currentTimeMillis的缓存类(见下方),难道System.currentTimeMillis的性能如此不堪吗?竟然要通过自定义的缓存时钟取而代之?

/** * 弱精度的计时器,考虑性能不使用同步策略。 *  * @author mycat */public class TimeUtil {    //当前毫秒数的缓存    private static volatile long CURRENT_TIME = System.currentTimeMillis();
public static final long currentTimeMillis() { return CURRENT_TIME; } public static final long currentTimeNanos() { return System.nanoTime(); } //更新缓存 public static final void update() { CURRENT_TIME = System.currentTimeMillis(); }
}//使用定时任务调度线程池,定期(每1s)调用update方法更新缓存时钟heartbeatScheduler.scheduleAtFixedRate(processorCheck(), 0L1000, TimeUnit.MILLISECONDS);


为了跟紧时代潮流,跟上性能优化“大师”们的步伐,我赶紧上网搜了一下“currentTimeMillis性能”,结果10个搜索结果里面有9个是关于system.currentTimeMillis性能问题的:


点开一看,这个说System.currentTimeMillis 比 new一个普通对象耗时还要高100倍左右,那个又拿出测试记录说System.currentTimeMillis并发情况下耗时比单线程调用高250倍


# 思索,System.currentTimeMillis有什么性能问题


看到这里,我恨不得马上打开IDEA,把代码里所有System.currentTimeMillis都给换掉,但是作为一个严谨的程序员,怎么能随波逐流,人云亦云呢?于是我仔细地拜读了这些文章,总结了他们的观点:


  • System.currentTimeMillis要访问系统时钟,这属于临界区资源,并发情况下必然导致多线程的争用
  • System.currentTimeMillis()之所以慢是因为去跟系统打了一次交道
  • 我有测试记录,并发耗时就是比单线程高250倍!


但我细品一番,发现这些观点充满了漏洞:


1.System.currentTimeMillis 确实要访问系统时钟,准确的说,是读取墙上时间(xtime),xtime是Linux系统给用户空间用来获取当前时间的,内核自己基本不会使用,只是维护更新。而且读写xtime使用的是Linux内核中的顺序锁,而非互斥锁,读线程间是互不影响的。

大家可以把顺序锁当成是解决了“ABA问题”的CompareAndSwap锁。对于一个临界区资源(这里是xtime),有一个操作序列号,写操作会使序列号+1,读操作则不会。

写操作:CAS使序列号+1

读操作:先获取序列号,读取数据,再获取一次序列号,前后两次获取的序列号相同,则证明进行读操作时没有写操作干扰,那么这次读是有效的,返回数据,否则说明读的时侯可能数据被更改了,这次读无效,重新做读操作。

大家可能有个疑问:读xtime的时候数据可能被更改吗?难度读操作不是原子性的吗?这是因为xtime是64位的,对于32位机器是需要分两次读的,而64位机器不会产生这个并发的问题。

2.跟系统打了一次交道,确实,用户进程必须进入内核态才能访问系统资源,但是,new一个对象,分配内存也属于系统调用,也要进内核态跟系统打交道,难道只是读一下系统的墙上时间,会比移动内存指针,初始化内存的耗时还要高100倍吗?匪夷所思


3.至于所谓的测试记录,给大家看一下他的测试代码:


这个测试代码的问题在于闭锁endLatch.countDown的耗时也被算进总体耗时了,闭锁是基于CAS实现的,在当前这样的计算密集型场景下,大量线程一拥而上,几乎都会因CAS失败而被挂起,大量线程挂起、排队、放下的耗时可不是小数目。其次使用这种方法(执行开始到执行完毕)来对比并发和单线程的调用耗时也有问题,单线程怎么和多线程比总的执行时间?能比的应该是每次调用的耗时之和才对(见下)

long begin = System.nanoTime();//单次调用System.currrentTimeMillis()long end = System.nanoTime();sum += end - begin;
记录每次调用的总耗时,这种方法虽然会把System.nanoTime()也算进总耗时里,但因为不论并发测试还是单线程测试都会记录System.nanoTime(),不会导致测试的不公平


# 数据说话,System.currentTimeMillis的性能没有问题


通过改进测试代码(测试代码见文末),并添加了优化“大师”们的缓存时钟做对比,我得到了以下数据:


System代表 System.currentTimeMillis

缓存时钟代表 使用静态成员变量做System.currentTimeMillis缓存的时钟类

200线程-Tomcat的默认线程数


使用JMH(Java基准测试框架)的测试结果


JMH按照推荐使用了双倍CPU的线程数(8线程),统计的是平均时间,测试代码见文末


测试结果分析


可以看到System.currentTimeMillis并发性能并不算差,在次数较少(短期并发调用)的情况下甚至比单线程要强很多,而在单线程调用时效率也要比缓存时钟要高一倍左右。实际环境中几乎是达不到上述测试中的多线程长时间并发调用System.currentTimeMillis这样的情况的,因而我认为没有必要对System.currentTimeMillis做所谓的“优化”。

这里没有做“new一个对象”的测试,是因为并不是代码里写了new Object(),JVM就会真的会给你在堆内存里new一个对象。这是JVM的一个编译优化——逃逸分析:先分析要创建的对象的作用域,如果这个对象只在一个method里有效(局部变量对象),则属于未 方法逃逸,不去实际创建对象,而是你在method里调了对象的哪个方法,就把这个方法的代码块内联进来。只在线程内有效则属于未 线程逃逸,会创建对象,但会自动消除我们做的无用的同步措施。


# 最后


纸上得来终觉浅,绝知此事要躬行


想要学习JMH,请跟着GitHub官方文档走,别人的博客可能跑不通就搬上去了,笔者也是刚刚踩过了这个坑


最后奉上我的测试代码


测试代码:

public class CurrentTimeMillisTest {
public static void main(String[] args) { int num = 10000000; System.out.print("单线程"+num+"次System.currentTimeMillis调用总耗时:"); System.out.println(singleThreadTest(() -> { long l = System.currentTimeMillis(); },num)); System.out.print("单线程"+num+"次CacheClock.currentTimeMillis调用总耗时:"); System.out.println(singleThreadTest(() -> { long l = CacheClock.currentTimeMillis(); },num)); System.out.print("并发"+num+"次System.currentTimeMillis调用总耗时:"); System.out.println(concurrentTest(() -> { long l = System.currentTimeMillis(); },num)); System.out.print("并发"+num+"次CacheClock.currentTimeMillis调用总耗时:"); System.out.println(concurrentTest(() -> { long l = CacheClock.currentTimeMillis(); },num)); }


/** * 单线程测试 * @return */ private static long singleThreadTest(Runnable runnable,int num) { long sum = 0; for (int i = 0; i < num; i++) { long begin = System.nanoTime(); runnable.run(); long end = System.nanoTime(); sum += end - begin; } return sum; }
/** * 并发测试 * @return */ private static long concurrentTest(Runnable runnable,int num) { ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(200,200,60, TimeUnit.SECONDS,new LinkedBlockingQueue<>(num)); long[] sum = new long[]{0}; //闭锁基于CAS实现,并不适合当前的计算密集型场景,可能导致等待时间较长 CountDownLatch countDownLatch = new CountDownLatch(num); for (int i = 0; i < num; i++) { threadPoolExecutor.submit(() -> { long begin = System.nanoTime(); runnable.run(); long end = System.nanoTime(); //计算复杂型场景更适合使用悲观锁 synchronized(CurrentTimeMillisTest.class) { sum[0] += end - begin; } countDownLatch.countDown(); }); } try { countDownLatch.await(); } catch (InterruptedException e) { e.printStackTrace(); } return sum[0]; }

/** * 缓存时钟,缓存System.currentTimeMillis()的值,每隔20ms更新一次 */ public static class CacheClock{ //定时任务调度线程池 private static ScheduledExecutorService timer = new ScheduledThreadPoolExecutor(1); //毫秒缓存 private static volatile long timeMilis; static { //每秒更新毫秒缓存 timer.scheduleAtFixedRate(new Runnable() { @Override public void run() { timeMilis = System.currentTimeMillis(); } },0,1000,TimeUnit.MILLISECONDS); } public static long currentTimeMillis() { return timeMilis; } }}

使用JMH的测试代码:

@BenchmarkMode(Mode.AverageTime)@OutputTimeUnit(TimeUnit.MICROSECONDS)//120轮预热,充分利用JIT的编译优化技术@Warmup(iterations = 120,time = 1,timeUnit = TimeUnit.MILLISECONDS)@Measurement(time = 1,timeUnit = TimeUnit.MICROSECONDS)//线程数:CPU*2(计算复杂型,也有CPU+1的说法)@Threads(8)@Fork(1)@State(Scope.Benchmark)public class JMHTest {
public static void main(String[] args) throws RunnerException { testNTime(10000); }
private static void testNTime(int num) throws RunnerException { Options options = new OptionsBuilder() .include(JMHTest.class.getSimpleName()) .measurementIterations(num) .output("E://testRecord.log") .build(); new Runner(options).run(); }

/** * System.currentMillisTime测试 * @return 将结果返回是为了防止死码消除(编译器将 无引用的变量 当成无用代码优化掉) */ @Benchmark public long testSystem() { return System.currentTimeMillis(); }
/** * 缓存时钟测试 * @return */ @Benchmark public long testCacheClock() { return JMHTest.CacheClock.currentTimeMillis(); }
/** * 缓存时钟,缓存System.currentTimeMillis()的值,每隔1s更新一次 */ public static class CacheClock{ private static ScheduledExecutorService timer = new ScheduledThreadPoolExecutor(1); private static volatile long timeMilis; static { timer.scheduleAtFixedRate(new Runnable() { @Override public void run() { timeMilis = System.currentTimeMillis(); } },0,1000,TimeUnit.MILLISECONDS); } public static long currentTimeMillis() { return timeMilis; } }}
来源 | https://juejin.cn/post/6887743425437925383

1. 三种方式,教你优雅的替换if-else语句!

2. Linux 运维必备 150 个命令,速度收藏~

3. 分享一个超全的基于jedis的Redis工具类

4. SpringBoot 的@Value注解太强大了,用了都说爽!

最近面试BAT,整理一份面试资料Java面试BATJ通关手册,覆盖了Java核心技术、JVM、Java并发、SSM、微服务、数据库、数据结构等等。

获取方式:点“在看”,关注公众号并回复 Java 领取,更多内容陆续奉上。

文章有帮助的话,在看,转发吧。

谢谢支持哟 (*^__^*)

浏览 36
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报