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