做者:LittleMagic
連接:https://www.jianshu.com/p/d2039190b1cbhtml
System.currentTimeMillis()是極其經常使用的基礎Java API,普遍地用來獲取時間戳或測量代碼執行時長等,在咱們的印象中應該快如閃電。java
但實際上在併發調用或者特別頻繁調用它的狀況下(好比一個業務繁忙的接口,或者吞吐量大的須要取得時間戳的流式程序),其性能表現會使人大跌眼鏡。linux
直接看下面的Demo。git
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"); } }
執行結果以下圖。github
可見,併發調用System.currentTimeMillis()一百次,耗費的時間是單線程調用一百次的250倍。spring
若是單線程的調用頻次增長(好比達到每毫秒數次的地步),也會觀察到相似的狀況。緩存
實際上在極端狀況下,System.currentTimeMillis()的耗時甚至會比建立一個簡單的對象實例還要多,看官能夠自行將上面線程中的語句換成new HashMap<>之類的試試看。bash
爲何會這樣呢?併發
來到HotSpot源碼的hotspot/src/os/linux/vm/os_linux.cpp文件中,有一個javaTimeMillis()方法,這就是System.currentTimeMillis()的native實現。intellij-idea
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() 這篇文章,我就不班門弄斧了。
http://pzemtsov.github.io/2017/07/23/the-slow-currenttimemillis.html
簡單來說就是:
HPET計時器性能較差的緣由是會將全部對時間戳的請求串行執行。TSC計時器性能較好,由於有專用的寄存器來保存時間戳。缺點是可能不穩定,由於它是純硬件的計時器,頻率可變(與處理器的CLK信號有關)。關於HPET和TSC的細節能夠參見:
https://en.wikipedia.org/wiki/High_Precision_Event_Timer
https://en.wikipedia.org/wiki/Time_Stamp_Counter
另外,能夠用如下的命令查看和修改時鐘源。
~ cat /sys/devices/system/clocksource/clocksource0/available_clocksource tsc hpet acpi_pm ~ cat /sys/devices/system/clocksource/clocksource0/current_clocksource tsc ~ echo 'hpet' > /sys/devices/system/clocksource/clocksource0/current_clocksource
如何解決這個問題?最多見的辦法是用單個調度線程來按毫秒更新時間戳,至關於維護一個全局緩存。其餘線程取時間戳時至關於從內存取,不會再形成時鐘資源的爭用,代價就是犧牲了一些精確度。具體代碼以下。
public class CurrentTimeMillisClock { private volatile long now; private CurrentTimeMillisClock() { this.now = System.currentTimeMillis(); scheduleTick(); } private void scheduleTick() { new ScheduledThreadPoolExecutor(1, runnable -> { Thread thread = new Thread(runnable, "current-time-millis"); thread.setDaemon(true); return thread; }).scheduleAtFixedRate(() -> { now = System.currentTimeMillis(); }, 1, 1, TimeUnit.MILLISECONDS); } public long now() { return now; } public static CurrentTimeMillisClock getInstance() { return SingletonHolder.INSTANCE; } private static class SingletonHolder { private static final CurrentTimeMillisClock INSTANCE = new CurrentTimeMillisClock(); } }
使用的時候,直接CurrentTimeMillisClock.getInstance().now()
就能夠了。不過,在System.currentTimeMillis()的效率沒有影響程序總體的效率時,就沒必要忙着作優化,這只是爲極端狀況準備的。
近期熱文推薦:
1.Java 15 正式發佈, 14 個新特性,刷新你的認知!!
2.終於靠開源項目弄到 IntelliJ IDEA 激活碼了,真香!
3.我用 Java 8 寫了一段邏輯,同事直呼看不懂,你試試看。。
以爲不錯,別忘了隨手點贊+轉發哦!