System.currentTimeMillis()的性能問題


一直覺得java原生API都是性能很高的,今天看一篇博客時,說到System.currentTimeMillis()的性能十分低下,覺得很奇怪,於是寫了一些代碼來嘗試了一下

public class CurrentTimeTest {
    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 : " + 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 : " + elapsedTime + " ns");
    }
}

執行結果如下:

 

可見System.currentTimeMoillis一百次耗費的時間非常大,尤其是並發狀態下比單線程高出一個量級,甚至極端情況比創建對象更耗費資源

查看HotSpot源碼的hotspot/src/os/linux/vm/os_linux.cpp,有一個javaTimeMillis方法,這就是System.currentTimeMillis的native實現。

jlong os::javaTimeMillis {
    timeval time;
    int status = gettimeofday(&time, NULL);
    assert(status != -1, "linux error");
    return jlong(time.tv_sec) * 1000 + jlong(time.tv_usec / 1000);
}

對於這部分源碼已經有國外大佬深入到了匯編的級別來探究,詳情可以參見《The Slow currentTimeMillis》

簡單來講就是:
    1.調用gettimeofday需要從用戶態切換到內核態;
    2.gettimeofday的表現受Linux系統的計時器(時鍾源)影響,在HPET計時器下性能尤其差;
    3.系統只有一個全局時鍾源,高並發或頻繁訪問會造成嚴重的爭用。

HPET計時器性能較差的原因是會將所有對時間戳的請求串行執行。TSC計時器性能較好,因為有專用的寄存器來保存時間戳。缺點是可能不穩定,因為它是純硬件的計時器,頻率可變(與處理器的CLK信號有關)。關於HPET和TSC的細節可以參見下面的鏈接,就不做過多討論
https://en.wikipedia.org/wiki/HighPrecisionEventTimer
https://en.wikipedia.org/wiki/TimeStamp_Counter

那么如何避免這個問題?最常見的辦法是用單個調度線程來按毫秒更新時間戳,相當於維護一個全局緩存。其他線程取時間戳時相當於從內存取,不會再造成時鍾資源的爭用,代價就是犧牲了一些精確度。具體代碼如下。

@Component
public class TimeServcie {
    private static long time;

    static {
        new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    try {
                        Thread.sleep(5);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    long cur = System.currentTimeMillis();
                    setTime(cur);
                }
            }
        }).start();
    }

    public static long getTime() {
        return time;
    }

    public static void setTime(long time) {
        TimeServcie.time = time;
    }
}

 


免責聲明!

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



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