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

共 4396字,需浏览 9分钟

 ·

2020-11-18 03:54

点击上方蓝色“程序猿DD”,选择“设为星标”

回复“资源”获取独家整理的学习资料!

作者:围军儿

来源:https://juejin.im/post/6887743425437925383

疑惑,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单线程缓存时钟200线程System200线程缓存时钟
1w3.682 ms42.844 ms0.583 ms0.444 ms
10w6.780 ms35.837 ms3.379 ms3.066 ms
100w30.764 ms70.917 ms36.416 ms27.906 ms
1000w263.287 ms427.319 ms355.452 ms261.360 ms

System代表 System.currentTimeMillis

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

200线程-Tomcat的默认线程数

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

测试次数\平均耗时\场景System缓存时钟
1w0.368 ± 0.667 微秒/次0.578 ± 1.039 微秒/次

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;
        }
    }
}


DD自研的沪牌代拍业务,点击直达


往期推荐

因退休太无聊,Python创始人加入微软!

HikariCP为什么自己造了一个FastList?

Spring Boot 2.4.0 正式发布!拥抱云原生!

服务网格仍然很难

10道棘手的Java面试题,看看你能答对几个?

如果MySQL磁盘满了,会发生什么?


扫一扫,关注我

一起学习,一起进步

每周赠书,福利不断

深度内容

推荐加入



浏览 16
点赞
评论
收藏
分享

手机扫一扫分享

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

手机扫一扫分享

分享
举报