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();比多线程并发执行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

 

上一篇:SpringBoot 异步任务处理


下一篇:java过滤器实现拦截多次恶意请求,封禁ip