一直覺得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; } }