System.currentTimeMillis 竟然存在性能问题??

👉 这是一个或许对你有用的社群

🐱 一对一交流/面试小册/简历优化/求职解惑,欢迎加入「芋道快速开发平台」知识星球。下面是星球提供的部分资料: 

61c2824052c104bd0e38690d76a95e1f.gif

👉这是一个或许对你有用的开源项目

国产 Star 破 10w+ 的开源项目,前端包括管理后台 + 微信小程序,后端支持单体和微服务架构。

功能涵盖 RBAC 权限、SaaS 多租户、数据权限、商城、支付、工作流、大屏报表、微信公众号等等功能:

  • Boot 地址:https://gitee.com/zhijiantianya/ruoyi-vue-pro

  • Cloud 地址:https://gitee.com/zhijiantianya/yudao-cloud

  • 视频教程:https://doc.iocoder.cn

来源: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(), 0L, 1000, TimeUnit.MILLISECONDS);
214adbea8a67d539c3a567cb7843f70e.png

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

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

f9c79863b467fdf4d1001fadf43ede4c.jpeg

基于 Spring Boot + MyBatis Plus + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能

  • 项目地址:https://github.com/YunaiV/ruoyi-vue-pro

  • 视频教程:https://doc.iocoder.cn/video/

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

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

  • System.currentTimeMillis要访问系统时钟 ,这属于临界区资源,并发情况下必然导致多线程的争用

  • System.currentTimeMillis()之所以慢是因为去跟系统打了一次交道

  • 有测试记录 ,并发耗时就是比单线程高250倍!

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

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

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

    写操作:CAS使序列号+1

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

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

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

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

    80b2e46d3b9d98a9ed73049d25d569c6.png

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

    long begin = System.nanoTime();
    //单次调用System.currrentTimeMillis()
    long end = System.nanoTime();
    sum += end - begin;

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

基于 Spring Cloud Alibaba + Gateway + Nacos + RocketMQ + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能

  • 项目地址:https://github.com/YunaiV/yudao-cloud

  • 视频教程:https://doc.iocoder.cn/video/

数据说话,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 微秒/次
601a42da5bb6dd6044487434106495b2.png

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

测试结果分析

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

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

c0552c6b7efada4c4c53ab47871ce154.jpeg

最后

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

想要学习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;
        }
    }
}

欢迎加入我的知识星球,全面提升技术能力。

👉 加入方式,长按”或“扫描”下方二维码噢

674b51c6c9a24fdab11e4887b02f5582.png

星球的内容包括:项目实战、面试招聘、源码解析、学习路线。

3d28fb5da7b98a302cae2ece5715d5c7.png

c238d025b0e64c73e998aeebc18fe91e.pngfa0e6a3f81fa8063aaf4af85d04aec09.pngfc55fb1ed0f866eb77f42d09c3c272b0.png4306f18f7fc5d233969201805dc9d81d.png

文章有帮助的话,在看,转发吧。
谢谢支持哟 (*^__^*)
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
编译原理是计算机专业的一门核心课程,旨在介绍编译程序构造的一般原理和基本方法。编译原理不仅是计算机科学理论的重要组成部分,也是实现高效、可靠的计算机程序设计的关键。本文将对编译原理的基本概念、发展历程、主要内容和实际应用进行详细介绍编译原理是计算机专业的一门核心课程,旨在介绍编译程序构造的一般原理和基本方法。编译原理不仅是计算机科学理论的重要组成部分,也是实现高效、可靠的计算机程序设计的关键。本文将对编译原理的基本概念、发展历程、主要内容和实际应用进行详细介绍编译原理是计算机专业的一门核心课程,旨在介绍编译程序构造的一般原理和基本方法。编译原理不仅是计算机科学理论的重要组成部分,也是实现高效、可靠的计算机程序设计的关键。本文将对编译原理的基本概念、发展历程、主要内容和实际应用进行详细介绍编译原理是计算机专业的一门核心课程,旨在介绍编译程序构造的一般原理和基本方法。编译原理不仅是计算机科学理论的重要组成部分,也是实现高效、可靠的计算机程序设计的关键。本文将对编译原理的基本概念、发展历程、主要内容和实际应用进行详细介绍编译原理是计算机专业的一门核心课程,旨在介绍编译程序构造的一般原理和基本

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值