<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          System.currentTimeMillis的性能真有如此不堪嗎?

          共 7968字,需瀏覽 16分鐘

           ·

          2021-10-20 07:23

          上一篇:0.2秒居然復(fù)制了100G文件?

          來源:https://juejin.cn/post/6887743425437925383

          疑惑,System.currentTimeMillis真有性能問題?


          最近我在研究一款中間件的源代碼時,發(fā)現(xiàn)它獲取當(dāng)前時間不是通過System.currentTimeMillis,而是通過自定義的System.currentTimeMillis的緩存類(見下方),難道System.currentTimeMillis的性能如此不堪嗎?竟然要通過自定義的緩存時鐘取而代之?

          /** * 弱精度的計時器,考慮性能不使用同步策略。 *  * @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(); }
          }//使用定時任務(wù)調(diào)度線程池,定期(每1s)調(diào)用update方法更新緩存時鐘heartbeatScheduler.scheduleAtFixedRate(processorCheck(), 0L, 1000, TimeUnit.MILLISECONDS);


          為了跟緊時代潮流,跟上性能優(yōu)化“大師”們的步伐,我趕緊上網(wǎng)搜了一下“currentTimeMillis性能”,結(jié)果10個搜索結(jié)果里面有9個是關(guān)于system.currentTimeMillis性能問題的:


          點(diǎn)開一看,這個說System.currentTimeMillis 比 new一個普通對象耗時還要高100倍左右,那個又拿出測試記錄說System.currentTimeMillis并發(fā)情況下耗時比單線程調(diào)用高250倍


          思索,System.currentTimeMillis有什么性能問題


          看到這里,我恨不得馬上打開IDEA,把代碼里所有System.currentTimeMillis都給換掉,但是作為一個嚴(yán)謹(jǐn)?shù)某绦騿T,怎么能隨波逐流,人云亦云呢?于是我仔細(xì)地拜讀了這些文章,總結(jié)了他們的觀點(diǎn):


          • System.currentTimeMillis要訪問系統(tǒng)時鐘,這屬于臨界區(qū)資源,并發(fā)情況下必然導(dǎo)致多線程的爭用
          • System.currentTimeMillis()之所以慢是因?yàn)槿?/span>跟系統(tǒng)打了一次交道
          • 我有測試記錄,并發(fā)耗時就是比單線程高250倍!


          但我細(xì)品一番,發(fā)現(xiàn)這些觀點(diǎn)充滿了漏洞:


          1.System.currentTimeMillis 確實(shí)要訪問系統(tǒng)時鐘,準(zhǔn)確的說,是讀取墻上時間(xtime),xtime是Linux系統(tǒng)給用戶空間用來獲取當(dāng)前時間的,內(nèi)核自己基本不會使用,只是維護(hù)更新。而且讀寫xtime使用的是Linux內(nèi)核中的順序鎖,而非互斥鎖,讀線程間是互不影響的。
          大家可以把順序鎖當(dāng)成是解決了“ABA問題”的CompareAndSwap鎖。對于一個臨界區(qū)資源(這里是xtime),有一個操作序列號,寫操作會使序列號+1,讀操作則不會。
          寫操作:CAS使序列號+1
          讀操作:先獲取序列號,讀取數(shù)據(jù),再獲取一次序列號,前后兩次獲取的序列號相同,則證明進(jìn)行讀操作時沒有寫操作干擾,那么這次讀是有效的,返回數(shù)據(jù),否則說明讀的時侯可能數(shù)據(jù)被更改了,這次讀無效,重新做讀操作。
          大家可能有個疑問:讀xtime的時候數(shù)據(jù)可能被更改嗎?難度讀操作不是原子性的嗎?這是因?yàn)閤time是64位的,對于32位機(jī)器是需要分兩次讀的,而64位機(jī)器不會產(chǎn)生這個并發(fā)的問題。

          2.跟系統(tǒng)打了一次交道,確實(shí),用戶進(jìn)程必須進(jìn)入內(nèi)核態(tài)才能訪問系統(tǒng)資源,但是,new一個對象,分配內(nèi)存也屬于系統(tǒng)調(diào)用,也要進(jìn)內(nèi)核態(tài)跟系統(tǒng)打交道,難道只是讀一下系統(tǒng)的墻上時間,會比移動內(nèi)存指針,初始化內(nèi)存的耗時還要高100倍嗎?匪夷所思


          3.至于所謂的測試記錄,給大家看一下他的測試代碼:



          這個測試代碼的問題在于閉鎖endLatch.countDown的耗時也被算進(jìn)總體耗時了,閉鎖是基于CAS實(shí)現(xiàn)的,在當(dāng)前這樣的計算密集型場景下,大量線程一擁而上,幾乎都會因CAS失敗而被掛起,大量線程掛起、排隊、放下的耗時可不是小數(shù)目。其次使用這種方法(執(zhí)行開始到執(zhí)行完畢)來對比并發(fā)和單線程的調(diào)用耗時也有問題,單線程怎么和多線程比總的執(zhí)行時間?能比的應(yīng)該是每次調(diào)用的耗時之和才對(見下)
          long begin = System.nanoTime();//單次調(diào)用System.currrentTimeMillis()long end = System.nanoTime();sum += end - begin;
          記錄每次調(diào)用的總耗時,這種方法雖然會把System.nanoTime()也算進(jìn)總耗時里,但因?yàn)椴徽摬l(fā)測試還是單線程測試都會記錄System.nanoTime(),不會導(dǎo)致測試的不公平


          數(shù)據(jù)說話,System.currentTimeMillis的性能沒有問題


          通過改進(jìn)測試代碼(測試代碼見文末),并添加了優(yōu)化“大師”們的緩存時鐘做對比,我得到了以下數(shù)據(jù):


          System代表 System.currentTimeMillis

          緩存時鐘代表 使用靜態(tài)成員變量做System.currentTimeMillis緩存的時鐘類

          200線程-Tomcat的默認(rèn)線程數(shù)


          使用JMH(Java基準(zhǔn)測試框架)的測試結(jié)果


          JMH按照推薦使用了雙倍CPU的線程數(shù)(8線程),統(tǒng)計的是平均時間,測試代碼見文末


          測試結(jié)果分析


          可以看到System.currentTimeMillis并發(fā)性能并不算差,在次數(shù)較少(短期并發(fā)調(diào)用)的情況下甚至比單線程要強(qiáng)很多,而在單線程調(diào)用時效率也要比緩存時鐘要高一倍左右。實(shí)際環(huán)境中幾乎是達(dá)不到上述測試中的多線程長時間并發(fā)調(diào)用System.currentTimeMillis這樣的情況的,因而我認(rèn)為沒有必要對System.currentTimeMillis做所謂的“優(yōu)化”。

          這里沒有做“new一個對象”的測試,是因?yàn)椴⒉皇谴a里寫了new Object(),JVM就會真的會給你在堆內(nèi)存里new一個對象。這是JVM的一個編譯優(yōu)化——逃逸分析:先分析要創(chuàng)建的對象的作用域,如果這個對象只在一個method里有效(局部變量對象),則屬于未 方法逃逸,不去實(shí)際創(chuàng)建對象,而是你在method里調(diào)了對象的哪個方法,就把這個方法的代碼塊內(nèi)聯(lián)進(jìn)來。只在線程內(nèi)有效則屬于未 線程逃逸,會創(chuàng)建對象,但會自動消除我們做的無用的同步措施。


          最后


          紙上得來終覺淺,絕知此事要躬行


          想要學(xué)習(xí)JMH,請跟著GitHub官方文檔走,別人的博客可能跑不通就搬上去了,筆者也是剛剛踩過了這個坑


          最后奉上我的測試代碼


          測試代碼:

          public class CurrentTimeMillisTest {
          public static void main(String[] args) { int num = 10000000; System.out.print("單線程"+num+"次System.currentTimeMillis調(diào)用總耗時:"); System.out.println(singleThreadTest(() -> { long l = System.currentTimeMillis(); },num)); System.out.print("單線程"+num+"次CacheClock.currentTimeMillis調(diào)用總耗時:"); System.out.println(singleThreadTest(() -> { long l = CacheClock.currentTimeMillis(); },num)); System.out.print("并發(fā)"+num+"次System.currentTimeMillis調(diào)用總耗時:"); System.out.println(concurrentTest(() -> { long l = System.currentTimeMillis(); },num)); System.out.print("并發(fā)"+num+"次CacheClock.currentTimeMillis調(diào)用總耗時:"); 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 < num; 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)前的計算密集型場景,可能導(dǎo)致等待時間較長 CountDownLatch countDownLatch = new CountDownLatch(num); for (int i = 0; i < num; i++) { threadPoolExecutor.submit(() -> { long begin = System.nanoTime(); runnable.run(); long end = System.nanoTime(); //計算復(fù)雜型場景更適合使用悲觀鎖 synchronized(CurrentTimeMillisTest.class) { sum[0] += end - begin; } countDownLatch.countDown(); }); } try { countDownLatch.await(); } catch (InterruptedException e) { e.printStackTrace(); } return sum[0]; }

          /** * 緩存時鐘,緩存System.currentTimeMillis()的值,每隔20ms更新一次 */ public static class CacheClock{ //定時任務(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(計算復(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(); }
          /** * 緩存時鐘測試 * @return */ @Benchmark public long testCacheClock() { return JMHTest.CacheClock.currentTimeMillis(); }
          /** * 緩存時鐘,緩存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; } }}

          感謝您的閱讀,也歡迎您發(fā)表關(guān)于這篇文章的任何建議,關(guān)注我,技術(shù)不迷茫!小編到你上高速。

              · END ·
          最后,關(guān)注公眾號互聯(lián)網(wǎng)架構(gòu)師,在后臺回復(fù):2T,可以獲取我整理的 Java 系列面試題和答案,非常齊全


          正文結(jié)束


          推薦閱讀 ↓↓↓

          1.不認(rèn)命,從10年流水線工人,到谷歌上班的程序媛,一位湖南妹子的勵志故事

          2.如何才能成為優(yōu)秀的架構(gòu)師?

          3.從零開始搭建創(chuàng)業(yè)公司后臺技術(shù)棧

          4.程序員一般可以從什么平臺接私活?

          5.37歲程序員被裁,120天沒找到工作,無奈去小公司,結(jié)果懵了...

          6.IntelliJ IDEA 2019.3 首個最新訪問版本發(fā)布,新特性搶先看

          7.這封“領(lǐng)導(dǎo)痛批95后下屬”的郵件,句句扎心!

          8.15張圖看懂瞎忙和高效的區(qū)別!


          瀏覽 84
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  成人精品国产 | 91极品在线 | 蜜臀尤物一区二区三区直播 | 亚洲欧美天堂 | 欧美成人在线免费 |