System.currentTimeMillis的性能真有如此不堪嗎?
點(diǎn)擊上方藍(lán)色“程序猿DD”,選擇“設(shè)為星標(biāo)”
回復(fù)“資源”獲取獨(dú)家整理的學(xué)習(xí)資料!

作者:圍軍兒
來源:https://juejin.im/post/6887743425437925383
疑惑,System.currentTimeMillis真有性能問題?
最近我在研究一款中間件的源代碼時(shí),發(fā)現(xiàn)它獲取當(dāng)前時(shí)間不是通過System.currentTimeMillis,而是通過自定義的System.currentTimeMillis的緩存類(見下方),難道System.currentTimeMillis的性能如此不堪嗎?竟然要通過自定義的緩存時(shí)鐘取而代之?
/**
?*?弱精度的計(jì)時(shí)器,考慮性能不使用同步策略。
?*?
?*?@author?mycat
?*/
public?class?TimeUtil?{
????//當(dāng)前毫秒數(shù)的緩存
????private?static?volatile?long?CURRENT_TIME?=?System.currentTimeMillis();
????public?static?final?long?currentTimeMillis()?{?return?CURRENT_TIME;?}
????public?static?final?long?currentTimeNanos()?{?return?System.nanoTime();?}
?//更新緩存
????public?static?final?void?update()?{?CURRENT_TIME?=?System.currentTimeMillis();?}
}
//使用定時(shí)任務(wù)調(diào)度線程池,定期(每1s)調(diào)用update方法更新緩存時(shí)鐘
heartbeatScheduler.scheduleAtFixedRate(processorCheck(),?0L,?1000,?TimeUnit.MILLISECONDS);

為了跟緊時(shí)代潮流,跟上性能優(yōu)化“大師”們的步伐,我趕緊上網(wǎng)搜了一下“currentTimeMillis性能”,結(jié)果10個(gè)搜索結(jié)果里面有9個(gè)是關(guān)于system.currentTimeMillis性能問題的:
點(diǎn)開一看,這個(gè)說System.currentTimeMillis?比 new一個(gè)普通對象耗時(shí)還要高100倍左右,那個(gè)又拿出測試記錄說System.currentTimeMillis并發(fā)情況下耗時(shí)比單線程調(diào)用高250倍

思索,System.currentTimeMillis有什么性能問題
看到這里,我恨不得馬上打開IDEA,把代碼里所有System.currentTimeMillis都給換掉,但是作為一個(gè)嚴(yán)謹(jǐn)?shù)某绦騿T,怎么能隨波逐流,人云亦云呢?于是我仔細(xì)地拜讀了這些文章,總結(jié)了他們的觀點(diǎn):
System.currentTimeMillis要訪問系統(tǒng)時(shí)鐘,這屬于臨界區(qū)資源,并發(fā)情況下必然導(dǎo)致多線程的爭用 System.currentTimeMillis()之所以慢是因?yàn)槿?strong style="color: rgb(255, 53, 2);line-height: 1.5;">跟系統(tǒng)打了一次交道 我有測試記錄,并發(fā)耗時(shí)就是比單線程高250倍!
但我細(xì)品一番,發(fā)現(xiàn)這些觀點(diǎn)充滿了漏洞:

System.currentTimeMillis 確實(shí)要訪問系統(tǒng)時(shí)鐘,準(zhǔn)確的說,是讀取墻上時(shí)間(xtime),xtime是Linux系統(tǒng)給用戶空間用來獲取當(dāng)前時(shí)間的,內(nèi)核自己基本不會使用,只是維護(hù)更新。而且讀寫xtime使用的是Linux內(nèi)核中的順序鎖,而非互斥鎖,讀線程間是互不影響的
大家可以把順序鎖當(dāng)成是解決了“ABA問題”的CompareAndSwap鎖。對于一個(gè)臨界區(qū)資源(這里是xtime),有一個(gè)操作序列號,寫操作會使序列號+1,讀操作則不會。
寫操作:CAS使序列號+1
讀操作:先獲取序列號,讀取數(shù)據(jù),再獲取一次序列號,前后兩次獲取的序列號相同,則證明進(jìn)行讀操作時(shí)沒有寫操作干擾,那么這次讀是有效的,返回?cái)?shù)據(jù),否則說明讀的時(shí)侯可能數(shù)據(jù)被更改了,這次讀無效,重新做讀操作。
大家可能有個(gè)疑問:讀xtime的時(shí)候數(shù)據(jù)可能被更改嗎?難度讀操作不是原子性的嗎?這是因?yàn)閤time是64位的,對于32位機(jī)器是需要分兩次讀的,而64位機(jī)器不會產(chǎn)生這個(gè)并發(fā)的問題。
跟系統(tǒng)打了一次交道,確實(shí),用戶進(jìn)程必須進(jìn)入內(nèi)核態(tài)才能訪問系統(tǒng)資源,但是,new一個(gè)對象,分配內(nèi)存也屬于系統(tǒng)調(diào)用,也要進(jìn)內(nèi)核態(tài)跟系統(tǒng)打交道,難道只是讀一下系統(tǒng)的墻上時(shí)間,會比移動(dòng)內(nèi)存指針,初始化內(nèi)存的耗時(shí)還要高100倍嗎?匪夷所思
至于所謂的測試記錄,給大家看一下他的測試代碼:

這個(gè)測試代碼的問題在于閉鎖endLatch.countDown的耗時(shí)也被算進(jìn)總體耗時(shí)了,閉鎖是基于CAS實(shí)現(xiàn)的,在當(dāng)前這樣的計(jì)算密集型場景下,大量線程一擁而上,幾乎都會因CAS失敗而被掛起,大量線程掛起、排隊(duì)、放下的耗時(shí)可不是小數(shù)目。其次使用這種方法(執(zhí)行開始到執(zhí)行完畢)來對比并發(fā)和單線程的調(diào)用耗時(shí)也有問題,單線程怎么和多線程比總的執(zhí)行時(shí)間?能比的應(yīng)該是每次調(diào)用的耗時(shí)之和才對(見下)
long?begin?=?System.nanoTime();
//單次調(diào)用System.currrentTimeMillis()
long?end?=?System.nanoTime();
sum?+=?end?-?begin;記錄每次調(diào)用的總耗時(shí),這種方法雖然會把System.nanoTime()也算進(jìn)總耗時(shí)里,但因?yàn)椴徽摬l(fā)測試還是單線程測試都會記錄System.nanoTime(),不會導(dǎo)致測試的不公平
數(shù)據(jù)說話,System.currentTimeMillis的性能沒有問題
通過改進(jìn)測試代碼(測試代碼見文末),并添加了優(yōu)化“大師”們的緩存時(shí)鐘做對比,我得到了以下數(shù)據(jù):
| 次數(shù)\耗時(shí)\場景 | 單線程System | 單線程緩存時(shí)鐘 | 200線程System | 200線程緩存時(shí)鐘 |
|---|---|---|---|---|
| 1w | 3.682 ms | 42.844 ms | 0.583 ms | 0.444 ms |
| 10w | 6.780 ms | 35.837 ms | 3.379 ms | 3.066 ms |
| 100w | 30.764 ms | 70.917 ms | 36.416 ms | 27.906 ms |
| 1000w | 263.287 ms | 427.319 ms | 355.452 ms | 261.360 ms |
System代表 System.currentTimeMillis
緩存時(shí)鐘代表 使用靜態(tài)成員變量做System.currentTimeMillis緩存的時(shí)鐘類
200線程-Tomcat的默認(rèn)線程數(shù)
使用JMH(Java基準(zhǔn)測試框架)的測試結(jié)果
| 測試次數(shù)\平均耗時(shí)\場景 | System | 緩存時(shí)鐘 |
|---|---|---|
| 1w | 0.368 ± 0.667 微秒/次 | 0.578 ± 1.039 微秒/次 |

JMH按照推薦使用了雙倍CPU的線程數(shù)(8線程),統(tǒng)計(jì)的是平均時(shí)間,測試代碼見文末
測試結(jié)果分析
可以看到System.currentTimeMillis并發(fā)性能并不算差,在次數(shù)較少(短期并發(fā)調(diào)用)的情況下甚至比單線程要強(qiáng)很多,而在單線程調(diào)用時(shí)效率也要比緩存時(shí)鐘要高一倍左右。實(shí)際環(huán)境中幾乎是達(dá)不到上述測試中的多線程長時(shí)間并發(fā)調(diào)用System.currentTimeMillis這樣的情況的,因而我認(rèn)為沒有必要對System.currentTimeMillis做所謂的“優(yōu)化”
這里沒有做“new一個(gè)對象”的測試,是因?yàn)椴⒉皇谴a里寫了new Object(),JVM就會真的會給你在堆內(nèi)存里new一個(gè)對象。這是JVM的一個(gè)編譯優(yōu)化——逃逸分析:先分析要?jiǎng)?chuàng)建的對象的作用域,如果這個(gè)對象只在一個(gè)method里有效(局部變量對象),則屬于未 方法逃逸,不去實(shí)際創(chuàng)建對象,而是你在method里調(diào)了對象的哪個(gè)方法,就把這個(gè)方法的代碼塊內(nèi)聯(lián)進(jìn)來。只在線程內(nèi)有效則屬于未 線程逃逸,會創(chuàng)建對象,但會自動(dòng)消除我們做的無用的同步措施。

最后
紙上得來終覺淺,絕知此事要躬行
想要學(xué)習(xí)JMH,請跟著GitHub官方文檔走,別人的博客可能跑不通就搬上去了,筆者也是剛剛踩過了這個(gè)坑
最后奉上我的測試代碼
測試代碼:
public?class?CurrentTimeMillisTest?{
????public?static?void?main(String[]?args)?{
????????int?num?=?10000000;
????????System.out.print("單線程"+num+"次System.currentTimeMillis調(diào)用總耗時(shí):????");
????????System.out.println(singleThreadTest(()?->?{
????????????long?l?=?System.currentTimeMillis();
????????},num));
????????System.out.print("單線程"+num+"次CacheClock.currentTimeMillis調(diào)用總耗時(shí):");
????????System.out.println(singleThreadTest(()?->?{
????????????long?l?=?CacheClock.currentTimeMillis();
????????},num));
????????System.out.print("并發(fā)"+num+"次System.currentTimeMillis調(diào)用總耗時(shí):??????");
????????System.out.println(concurrentTest(()?->?{
????????????long?l?=?System.currentTimeMillis();
????????},num));
????????System.out.print("并發(fā)"+num+"次CacheClock.currentTimeMillis調(diào)用總耗時(shí):??");
????????System.out.println(concurrentTest(()?->?{
????????????long?l?=?CacheClock.currentTimeMillis();
????????},num));
????}
????/**
?????*?單線程測試
?????*?@return
?????*/
????private?static?long?singleThreadTest(Runnable?runnable,int?num)?{
????????long?sum?=?0;
????????for?(int?i?=?0;?i?????????????long?begin?=?System.nanoTime();
????????????runnable.run();
????????????long?end?=?System.nanoTime();
????????????sum?+=?end?-?begin;
????????}
????????return?sum;
????}
????/**
?????*?并發(fā)測試
?????*?@return
?????*/
????private?static?long?concurrentTest(Runnable?runnable,int?num)?{
????????ThreadPoolExecutor?threadPoolExecutor?=?new?ThreadPoolExecutor(200,200,60,?TimeUnit.SECONDS,new?LinkedBlockingQueue<>(num));
????????long[]?sum?=?new?long[]{0};
????????//閉鎖基于CAS實(shí)現(xiàn),并不適合當(dāng)前的計(jì)算密集型場景,可能導(dǎo)致等待時(shí)間較長
????????CountDownLatch?countDownLatch?=?new?CountDownLatch(num);
????????for?(int?i?=?0;?i?????????????threadPoolExecutor.submit(()?->?{
????????????????long?begin?=?System.nanoTime();
????????????????runnable.run();
????????????????long?end?=?System.nanoTime();
????????????????//計(jì)算復(fù)雜型場景更適合使用悲觀鎖
????????????????synchronized(CurrentTimeMillisTest.class)?{
????????????????????sum[0]?+=?end?-?begin;
????????????????}
????????????????countDownLatch.countDown();
????????????});
????????}
????????try?{
????????????countDownLatch.await();
????????}?catch?(InterruptedException?e)?{
????????????e.printStackTrace();
????????}
????????return?sum[0];
????}
????/**
?????*?緩存時(shí)鐘,緩存System.currentTimeMillis()的值,每隔20ms更新一次
?????*/
????public?static?class?CacheClock{
????????//定時(shí)任務(wù)調(diào)度線程池
????????private?static?ScheduledExecutorService?timer?=?new?ScheduledThreadPoolExecutor(1);
????????//毫秒緩存
????????private?static?volatile?long?timeMilis;??????
????????static?{
????????????//每秒更新毫秒緩存
????????????timer.scheduleAtFixedRate(new?Runnable()?{
????????????????@Override
????????????????public?void?run()?{
????????????????????timeMilis?=?System.currentTimeMillis();
????????????????}
????????????},0,1000,TimeUnit.MILLISECONDS);
????????}
????????
????????public?static?long?currentTimeMillis()?{
????????????return?timeMilis;
????????}
????}
}
使用JMH的測試代碼:
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MICROSECONDS)
//120輪預(yù)熱,充分利用JIT的編譯優(yōu)化技術(shù)
@Warmup(iterations?=?120,time?=?1,timeUnit?=?TimeUnit.MILLISECONDS)
@Measurement(time?=?1,timeUnit?=?TimeUnit.MICROSECONDS)
//線程數(shù):CPU*2(計(jì)算復(fù)雜型,也有CPU+1的說法)
@Threads(8)
@Fork(1)
@State(Scope.Benchmark)
public?class?JMHTest?{
????public?static?void?main(String[]?args)?throws?RunnerException?{
????????testNTime(10000);
????}
????private?static?void?testNTime(int?num)?throws?RunnerException?{
????????Options?options?=?new?OptionsBuilder()
????????????????.include(JMHTest.class.getSimpleName())
????????????????.measurementIterations(num)
????????????????.output("E://testRecord.log")
????????????????.build();
????????new?Runner(options).run();
????}
????/**
?????*?System.currentMillisTime測試
?????*?@return?將結(jié)果返回是為了防止死碼消除(編譯器將?無引用的變量?當(dāng)成無用代碼優(yōu)化掉)
?????*/
????@Benchmark
????public?long?testSystem()?{
????????return?System.currentTimeMillis();
????}
????/**
?????*?緩存時(shí)鐘測試
?????*?@return
?????*/
????@Benchmark
????public?long?testCacheClock()?{
????????return?JMHTest.CacheClock.currentTimeMillis();
????}
????/**
?????*?緩存時(shí)鐘,緩存System.currentTimeMillis()的值,每隔1s更新一次
?????*/
????public?static?class?CacheClock{
????????private?static?ScheduledExecutorService?timer?=?new?ScheduledThreadPoolExecutor(1);
????????private?static?volatile?long?timeMilis;
????????static?{
????????????timer.scheduleAtFixedRate(new?Runnable()?{
????????????????@Override
????????????????public?void?run()?{
????????????????????timeMilis?=?System.currentTimeMillis();
????????????????}
????????????},0,1000,TimeUnit.MILLISECONDS);
????????}
????????public?static?long?currentTimeMillis()?{
????????????return?timeMilis;
????????}
????}
}
DD自研的滬牌代拍業(yè)務(wù),點(diǎn)擊直達(dá)
往期推薦
掃一掃,關(guān)注我
一起學(xué)習(xí),一起進(jìn)步
每周贈(zèng)書,福利不斷
﹀
﹀
﹀
深度內(nèi)容
推薦加入




