• System.currentTimeMillis()的性能问题以及解决方法


    System.currentTimeMillis()是极其常用的基础Java API,广泛地用来获取时间戳或测量代码执行时长等,在我们的印象中应该快如闪电。但实际上在并发调用或者特别频繁调用它的情况下(比如一个业务繁忙的接口,或者吞吐量大的需要取得时间戳的流式程序),其性能表现会令人大跌眼镜。

    public class CurrentTimeMillisPerfDemo {
    
        private static final int COUNT = 100;
    
        public static void main(String[] args) throws Exception {
    
            long beginTime = System.nanoTime();
    
            for (int i = 0; i < COUNT; i++) {
                System.currentTimeMillis();
            }
    
            long elapsedTime = System.nanoTime() - beginTime;
            System.out.println("100 System.currentTimeMillis() serial calls: " + elapsedTime + " ns");
    
            CountDownLatch startLatch = new CountDownLatch(1);
            CountDownLatch endLatch = new CountDownLatch(COUNT);
    
            for (int i = 0; i < COUNT; i++) {
                new Thread(() -> {
                    try {
                           startLatch.await();
                           System.currentTimeMillis();
                         } catch (InterruptedException e) {
                                e.printStackTrace();
                            } finally {
                                endLatch.countDown();
                            }
                        }).start();
            }
            beginTime = System.nanoTime();
            startLatch.countDown();
            endLatch.await();
            elapsedTime = System.nanoTime() - beginTime;
            System.out.println("100 System.currentTimeMillis() parallel calls: " + elapsedTime + " ns");
        }
    }

    在这里插入图片描述
    可见而知,单线程执行System.currentTimeMillis();比多线程并发执行System.currentTimeMillis();快了许多倍。

    为什么会这样?

    来到HotSpot源码的hotspot/src/os/linux/vm/os_linux.cpp文件中,有一个javaTimeMillis()方法,这就是System.currentTimeMillis()的native实现。

    挖源码就到此为止,因为已经有国外大佬深入到了汇编的级别来探究,详情可以参见《The Slow currentTimeMillis()》这篇文章。简单来讲就是:

    调用gettimeofday()需要从用户态切换到内核态;
    gettimeofday()的表现受Linux系统的计时器(时钟源)影响,在HPET计时器下性能尤其差;
    系统只有一个全局时钟源,高并发或频繁访问会造成严重的争用。
    HPET计时器性能较差的原因是会将所有对时间戳的请求串行执行。TSC计时器性能较好,因为有专用的寄存器来保存时间戳。缺点是可能不稳定,因为它是纯硬件的计时器,频率可变(与处理器的CLK信号有关)。关于HPET和TSC的细节可以参见https://en.wikipedia.org/wiki/HighPrecisionEventTimer与https://en.wikipedia.org/wiki/TimeStamp_Counter。

    如何解决这个问题?
    最常见的办法是用单个调度线程来按毫秒更新时间戳,相当于维护一个全局缓存。其他线程取时间戳时相当于从内存取,不会再造成时钟资源的争用,代价就是牺牲了一些精确度。具体代码如下。

    public class SystemClock {
        private static final SystemClock MILLIS_CLOCK = new SystemClock(1);
        private final long precision;
        private final AtomicLong now;
    
        private SystemClock(long precision) {
            this.precision = precision;
            now = new AtomicLong(System.currentTimeMillis());
            scheduleClockUpdating();
        }
    
        public static SystemClock millisClock() {
            return MILLIS_CLOCK;
        }
    
        private void scheduleClockUpdating() {
            ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(runnable -> {
                Thread thread = new Thread(runnable, "system.clock");
                thread.setDaemon(true);
                return thread;
            });
            scheduler.scheduleAtFixedRate(() -> now.set(System.currentTimeMillis()), precision, precision, TimeUnit.MILLISECONDS);
        }
    
        public long now() {
            return now.get();
        }
    }

    可以使用并发量大的情况下SystemClock.millisClock().now()输出当前时间,有一定精度上问题,得到是时间获取上效率。

    静态内部类写法

    package cn.ucaner.alpaca.common.util.key;
    
    import java.sql.Timestamp;
    import java.util.concurrent.*;
    import java.util.concurrent.atomic.AtomicLong;
    
    /**
     * 高并发场景下System.currentTimeMillis()的性能问题的优化
     * <p><p>
     * System.currentTimeMillis()的调用比new一个普通对象要耗时的多(具体耗时高出多少我还没测试过,有人说是100倍左右)<p>
     * System.currentTimeMillis()之所以慢是因为去跟系统打了一次交道<p>
     * 后台定时更新时钟,JVM退出时,线程自动回收<p>
     * 10亿:43410,206,210.72815533980582%<p>
     * 1亿:4699,29,162.0344827586207%<p>
     * 1000万:480,12,40.0%<p>
     * 100万:50,10,5.0%<p>
     * @author lry
     */
    public class SystemClock {
    
        private final long period;
    
        private final AtomicLong now;
    
        ExecutorService executor = Executors.newSingleThreadExecutor();
    
        private SystemClock(long period) {
            this.period = period;
            this.now = new AtomicLong(System.currentTimeMillis());
            scheduleClockUpdating();
        }
    
        private static class InstanceHolder {
            public static final SystemClock INSTANCE = new SystemClock(1);
        }
    
        private static SystemClock instance() {
            return InstanceHolder.INSTANCE;
        }
    
        private void scheduleClockUpdating() {
            ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(new ThreadFactory() {
                @Override
                public Thread newThread(Runnable runnable) {
                    Thread thread = new Thread(runnable, "System Clock");
                    thread.setDaemon(true);
                    return thread;
                }
            });
            scheduler.scheduleAtFixedRate(new Runnable() {
                @Override
                public void run() {
                    now.set(System.currentTimeMillis());
                }
            }, period, period, TimeUnit.MILLISECONDS);
        }
    
        private long currentTimeMillis() {
            return now.get();
        }
    
        public static long now() {
            return instance().currentTimeMillis();
        }
    
        public static String nowDate() {
            return new Timestamp(instance().currentTimeMillis()).toString();
        }
    
        /**
         * @Description: Just for test
         * @param args void
         * @throws InterruptedException
         * @Autor: Jason - jasonandy@hotmail.com
         */
        public static void main(String[] args) throws InterruptedException {
            for (int i = 0; i < 100; i++) {
                System.out.println(nowDate());
                Thread.sleep(1000);
            }
        }
    }
    //Outputs
    //2018-05-10 15:37:18.774
    //2018-05-10 15:37:19.784
    //2018-05-10 15:37:20.784
    //2018-05-10 15:37:21.785
    //2018-05-10 15:37:22.784
    //2018-05-10 15:37:23.784
    //2018-05-10 15:37:24.785
    //2018-05-10 15:37:25.784
    //2018-05-10 15:37:26.785
    //2018-05-10 15:37:27.786
    //2018-05-10 15:37:28.785
    //2018-05-10 15:37:29.785
    //2018-05-10 15:37:30.785
    //2018-05-10 15:37:31.785
  • 相关阅读:
    nginx proxy_cache 缓存配置[转]
    MongoDB使用小结:一些常用操作分享
    PHP读取Mongodb数据报错,Cannot natively represent the long 8331412483000 on this platform
    MongoDB 学习笔记(python操作)
    Python 中 os.path模板
    Python 优雅的操作字典【转】
    MongoDB 一对多关系建模
    nginx之location配置
    MongoDB安装Windows服务
    .net4.0 请求HTTPS出错:未能创建 SSL/TLS 安全通道
  • 原文地址:https://www.cnblogs.com/makai/p/15494281.html
Copyright © 2020-2023  润新知