日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

了解这些坑,再也不会出现诡异的BUG了~

發(fā)布時間:2025/3/16 编程问答 24 豆豆
生活随笔 收集整理的這篇文章主要介紹了 了解这些坑,再也不会出现诡异的BUG了~ 小編覺得挺不錯的,現(xiàn)在分享給大家,幫大家做個參考.

前言

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

?

幕后那些事

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

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

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

  • 操作系統(tǒng)增加了進程、線程,以分時復用 CPU,進而均衡 CPU 與 I/O 設備的速度差異;

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

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

    ?

    坑一:CPU緩存導致的可見性問題

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

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

    因為所有的線程都是在同一個CPU緩存中讀寫數(shù)據(jù),一個線程對緩存的寫,對于另外一個線程肯定是可見的。如下圖:

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

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

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

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

    從上圖可以很清楚的了解,線程A和線程B對變量A的改變是不可見的,因為是在兩個不同的CPU緩存中。

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

    public?class?TestThread?{private?Long?total=0L;//循環(huán)一萬次相加private?void?add(){for?(int?i?=?0;?i?<?10000;?i++)?{total+=1;}}//開啟兩個線程相加public?static?void?calc()?throws?InterruptedException?{TestThread?thread=new?TestThread();//創(chuàng)建兩個線程Thread?thread1=new?Thread(thread::add);Thread?thread2=new?Thread(thread::add);//啟動線程thread1.start();thread2.start();//阻塞主線程thread1.join();thread2.join();System.out.println(thread.total);}

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

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

    注意:如果循環(huán)的次數(shù)很少,這種情況不是很明顯,如果次數(shù)設置的越大,則結果越明顯,因為兩個線程不是同時啟動的。

    ?

    坑二:線程切換導致的原子性問題

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

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

    任務切換

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

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

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

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

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

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

  • 指令3:將結果寫入內(nèi)存(緩存機制可能導致寫入的是CPU緩存而不是內(nèi)存)

  • 基于CPU指令是什么意思呢?簡單的說就是任務切換的時機可能是上面的任何一條指令完成之后。

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

    非原子操作

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

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

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

    ?

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

    編譯優(yōu)化算是最詭異的一個難題了,雖然高級語言規(guī)定了代碼的執(zhí)行順序,但是編譯器有時為了優(yōu)化性能,則會改變代碼執(zhí)行的順序,比如a=4;b=3;,在代碼中可能給人直觀的感受是a=4先執(zhí)行,b=3后執(zhí)行,但是編譯器可能為了優(yōu)化性能,先執(zhí)行了b=3,這種對于我們?nèi)庋凼遣豢梢姷?#xff0c;上面例子中雖然不影響結果,但是有時候編譯器的優(yōu)化可能導致意想不到的BUG。

    雙重校驗鎖實現(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關鍵字,那么此時這個代碼在并發(fā)的情況下有問題嗎?

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

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

  • 在內(nèi)存N上初始化Singleton對象

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

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

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

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

  • 在內(nèi)存N上初始化Singleton對象

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

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

    單例NPE

    ?

    總結

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

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

  • CPU緩存導致的可見性問題

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

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

  • 有道無術,術可成;有術無道,止于術

    歡迎大家關注Java之道公眾號

    好文章,我在看??

    新人創(chuàng)作打卡挑戰(zhàn)賽發(fā)博客就能抽獎!定制產(chǎn)品紅包拿不停!

    總結

    以上是生活随笔為你收集整理的了解这些坑,再也不会出现诡异的BUG了~的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網(wǎng)站內(nèi)容還不錯,歡迎將生活随笔推薦給好友。