<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>

          了解這些坑,再也不會(huì)出現(xiàn)詭異的BUG了~

          共 926字,需瀏覽 2分鐘

           ·

          2021-01-23 10:15

          前言

          在高并發(fā)的情況下,你的程序是不是經(jīng)常出現(xiàn)一些詭異的BUG,每次都是花費(fèi)大量時(shí)間排查,但是你有沒有思考過這一切罪惡的源頭是什么呢?

          ?

          幕后那些事

          CPU內(nèi)存I/O設(shè)備的速度差異越來越大,這也是程序性能的瓶頸,根據(jù)木桶理論,最終決定程序的整體性能取決于最慢的操作-讀寫I/O設(shè)備,單方面的提高CPU的性能是無用的。

          為了平衡三者的差距,大牛前輩們不斷努力,最終做出了卓越的貢獻(xiàn):

          1. CPU增加了緩存,平衡與內(nèi)存之間的速度差異

          2. 操作系統(tǒng)增加了進(jìn)程、線程,以分時(shí)復(fù)用 CPU,進(jìn)而均衡 CPUI/O 設(shè)備的速度差異;

          3. 編譯程序優(yōu)化指令執(zhí)行次序,使得緩存能夠得到更加合理地利用。

          注意:正是硬件前輩們做的這些貢獻(xiàn),額外的后果需要軟件工程師來承擔(dān),太坑了。

          ?

          坑一:CPU緩存導(dǎo)致的可見性問題

          在單核CPU的時(shí)代,所有的線程都在單個(gè)CPU上執(zhí)行,不存在CPU數(shù)據(jù)和內(nèi)存的數(shù)據(jù)的一致性。

          一個(gè)線程對(duì)共享變量的修改,另外一個(gè)線程能夠立刻看到,我們稱為可見性。

          因?yàn)樗械木€程都是在同一個(gè)CPU緩存中讀寫數(shù)據(jù),一個(gè)線程對(duì)緩存的寫,對(duì)于另外一個(gè)線程肯定是可見的。如下圖:

          單核CPU與內(nèi)存關(guān)系


          從上圖可以很清楚的了解,線程A對(duì)于變量的修改都是在同一個(gè)CPU緩存中,則線程B肯定是可見的。

          但是多核時(shí)代的到來則意味著每個(gè)CPU上都有一個(gè)獨(dú)立的緩存,信息不再互通了,此時(shí)保證內(nèi)存和CPU緩存的一致性就很難了。如下圖:

          雙核CPU與內(nèi)存關(guān)系


          從上圖可以很清楚的了解,線程A和線程B對(duì)變量A的改變是不可見的,因?yàn)槭窃趦蓚€(gè)不同的CPU緩存中。

          最簡單的證明方式則是在多核CPU的電腦上跑一個(gè)循環(huán)相加的方法,同時(shí)開啟兩個(gè)線程運(yùn)行,最終得到的結(jié)果肯定不是正確的,如下:

          public?class?TestThread?{
          ????private?Long?total=0L;
          ????//循環(huán)一萬次相加
          ????private?void?add(){
          ????????for?(int?i?=?0;?i?10000
          ;?i++)?{
          ????????????total+=1;
          ????????}
          ????}

          ????//開啟兩個(gè)線程相加
          ????public?static?void?calc()?throws?InterruptedException?{
          ????????TestThread?thread=new?TestThread();
          ????????//創(chuàng)建兩個(gè)線程
          ????????Thread?thread1=new?Thread(thread::add);
          ????????Thread?thread2=new?Thread(thread::add);

          ????????//啟動(dòng)線程
          ????????thread1.start();
          ????????thread2.start();

          ????????//阻塞主線程
          ????????thread1.join();
          ????????thread2.join();
          ????????System.out.println(thread.total);
          ????}

          上述代碼在單核CPU的電腦上運(yùn)行的結(jié)果肯定是20000,但是在多核CPU的電腦上運(yùn)行的結(jié)果則是在10000~20000之間,為什么呢?

          原因很簡單,第一次在兩個(gè)線程啟動(dòng)后,會(huì)將total=0讀取到各自的CPU緩存中,執(zhí)行total+1=0后,各自將得到的結(jié)果total=1寫入到內(nèi)存中(理想中應(yīng)該是total=2),由于各自的CPU緩存中都有了值,因此每個(gè)線程都是基于各自CPU緩存中的值來計(jì)算,因此最終導(dǎo)致了寫入內(nèi)存中的值是在10000~20000之間。

          注意:如果循環(huán)的次數(shù)很少,這種情況不是很明顯,如果次數(shù)設(shè)置的越大,則結(jié)果越明顯,因?yàn)閮蓚€(gè)線程不是同時(shí)啟動(dòng)的。

          ?

          坑二:線程切換導(dǎo)致的原子性問題

          早期的操作系統(tǒng)是基于進(jìn)程調(diào)度CPU,不同進(jìn)程間是共享內(nèi)存空間的,比如你在IDEA寫代碼的同時(shí),能夠打開QQ音樂,這個(gè)就是多進(jìn)程。

          操作系統(tǒng)允許某個(gè)進(jìn)程執(zhí)行一段時(shí)間,比如40毫秒,過了這個(gè)時(shí)間則會(huì)選擇另外一個(gè)進(jìn)程,這個(gè)過程稱之為任務(wù)切換,這個(gè)40毫秒稱之為時(shí)間片,如下圖:

          任務(wù)切換


          在一個(gè)時(shí)間片內(nèi),如果一個(gè)進(jìn)程進(jìn)行IO操作,比如讀文件,這個(gè)時(shí)候該進(jìn)程可以把自己標(biāo)記為休眠狀態(tài)并讓出CPU的使用權(quán),待文件讀進(jìn)內(nèi)存,操作系統(tǒng)會(huì)將這個(gè)休眠的進(jìn)程喚醒,喚醒后的進(jìn)程就有機(jī)會(huì)重新獲得CPU的使用權(quán)。

          現(xiàn)代的操作系統(tǒng)更加輕量級(jí)了,都是基于線程調(diào)度,現(xiàn)在提到的任務(wù)切換大都指示線程切換

          注意:操作系統(tǒng)進(jìn)行任務(wù)切換是基于CPU指令

          基于CPU指令是什么意思呢?Java作為高級(jí)編程語言,一條簡單的語句可能底層就需要多條CPU指令,例如total+=1這條語句,至少需要三條CPU指令,如下:

          1. 指令1:將total從內(nèi)存讀到CPU寄存器中

          2. 指令2:在寄存器中執(zhí)行+1

          3. 指令3:將結(jié)果寫入內(nèi)存(緩存機(jī)制可能導(dǎo)致寫入的是CPU緩存而不是內(nèi)存)

          基于CPU指令是什么意思呢?簡單的說就是任務(wù)切換的時(shí)機(jī)可能是上面的任何一條指令完成之后。

          我們假設(shè)在線程A執(zhí)行了指令1后做了任務(wù)切換,此時(shí)線程B執(zhí)行,雖然執(zhí)行了total+=1,但是最終的結(jié)果卻不是2,如下圖:

          非原子操作


          我們把一個(gè)或者多個(gè)操作在CPU執(zhí)行過程中不被中斷的特性稱之為原子性。

          注意:CPU僅僅能保證CPU指令執(zhí)行的原子性,并不能保證高級(jí)語言的單條語句的原子性。

          此處分享一道經(jīng)典的面試題:Long類型的數(shù)據(jù)在32位操作系統(tǒng)中加減是否存在并發(fā)問題?答案:是,因?yàn)?/span>Long類型是64位,在32位的操作系統(tǒng)中執(zhí)行加減肯定是要拆分成多個(gè)CPU指令,因此無法保證加減的原子性。

          ?

          坑三:編譯優(yōu)化帶來的有序性問題

          編譯優(yōu)化算是最詭異的一個(gè)難題了,雖然高級(jí)語言規(guī)定了代碼的執(zhí)行順序,但是編譯器有時(shí)為了優(yōu)化性能,則會(huì)改變代碼執(zhí)行的順序,比如a=4;b=3;,在代碼中可能給人直觀的感受是a=4先執(zhí)行,b=3后執(zhí)行,但是編譯器可能為了優(yōu)化性能,先執(zhí)行了b=3,這種對(duì)于我們?nèi)庋凼遣豢梢姷模厦胬又须m然不影響結(jié)果,但是有時(shí)候編譯器的優(yōu)化可能導(dǎo)致意想不到的BUG。

          雙重校驗(yàn)鎖實(shí)現(xiàn)單例不知大家有沒有聽說過,代碼如下:

          public?class?Singleton?{
          ??static?Singleton?instance;
          ??static?Singleton?getInstance(){
          ????if?(instance?==?null)?{
          ??????synchronized(Singleton.class)?{
          ????????if?(instance?==?null)
          ??????????instance?=?new?Singleton();
          ????????}
          ????}
          ????return?instance;
          ??}
          }

          這里我去掉了volatile關(guān)鍵字,那么此時(shí)這個(gè)代碼在并發(fā)的情況下有問題嗎?

          上述代碼看上去很完美,但是最大的問題就在new Singleton();這行代碼上,預(yù)期中的new操作順序如下:

          1. 分配一塊內(nèi)存N

          2. 在內(nèi)存N上初始化Singleton對(duì)象

          3. 將內(nèi)存N的地址賦值給instance變量

          但是實(shí)際上編譯優(yōu)化后的執(zhí)行順序如下:

          1. 分配一塊內(nèi)存N

          2. 將內(nèi)存N的地址賦值給instance變量

          3. 在內(nèi)存N上初始化Singleton對(duì)象

          很多人問了,優(yōu)化后影響了什么?

          將內(nèi)存N的地址提前賦值給instance變量意味著instance!=null是成立的,一旦是高并發(fā)的情況下,線程A執(zhí)行第二步發(fā)生了任務(wù)切換,則線程B執(zhí)行到了if (instance == null)這個(gè)判斷,此時(shí)不成立,則直接返回了instance,但是此時(shí)的instance并沒有初始化過,如果此時(shí)訪問其中的成員變量則會(huì)發(fā)生空指針異常,執(zhí)行流程如下圖:

          單例NPE

          ?

          總結(jié)

          并發(fā)編程是區(qū)分高低手的門檻,只有深刻理解三大特性:可見性原子性有序性才能解決詭異的BUG

          本文分析了帶來這三大特性源頭,如下:

          1. CPU緩存導(dǎo)致的可見性問題

          2. 線程切換帶來的原子性問題

          3. 編譯優(yōu)化帶來的有序性問題


          有道無術(shù),術(shù)可成;有術(shù)無道,止于術(shù)

          歡迎大家關(guān)注Java之道公眾號(hào)


          好文章,我在看??

          瀏覽 30
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <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>
                  中文字幕在线字幕中文乱码区别 | 做爱下载视频免费网站 | 免费日韩毛片 | 美女操逼免费网站 | 国产一线二线三线在线 |