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

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM