併發安全問題的源頭

0.定義

可見性: 一個線程對共享變量的修改,另一個線程能夠立刻看到。
原子性: 一個或多個操作在CPU執行過程中不被中斷,稱爲原子性。
有序性: 程序按照代碼的先後順序執行。

  • 導致可見性問題的原因是CPU緩存;
  • 導致有序性問題的原因是編譯優化。
  • 線程切換可能帶來原子性問題
    解決問題的直接方法就是禁用緩存和優化。Java內存模型JVM如何按需禁用緩存和編譯優化的方法。
    具體來說這些方法包括 volatile、synchronized 和final三個關鍵字,以及六項Happens-Before

1.緩存導致的可見性問題

如果是單核CPU,所有的線程都在一個CPU上運行,那麼CPU緩存和內存的一致性很容易解決。多個線程操作的都是CPU中的同一個值

在這裏插入圖片描述
現在是多核CPU時代,每個CPU都有自己的緩存。當多個線程在不同CPU上執行時,這些線程操作的是不同的CPU緩存。這時候A線程對共享變量的操作,對B線程就不具備可見性了。
在這裏插入圖片描述

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

操作系統允許某個進程執行一小段時間。例如當一個進程在執行IO操作時,進程把自己標記爲“休眠中”以讓出CPU,等文件讀入內存之後,操作系統會將整個進程喚醒,喚醒後的進程有機會重新獲取CPU的使用權了。讓出CPU是利用IO操作的時間,讓CPU可以幹其他事,提高CPU使用率。
Java語言中併發實現都是基於線程的,涉及到線程切換。高級語言中的一條語句,往往需要多條CPU指令來完成。比如i++操作有3條CPU指令:
1.將變量i的值從內存加載到CPU的寄存器
2.在寄存器中完成+1操作
3.將結果寫入內存(或者CPU緩存)

操作系統的任務切換,可能在任意一條CPU指令完成時執行。這個的結果就是兩個線程各自在自己的CPU完成了基於原始值的+1操作,而不是總體+2。
在這裏插入圖片描述

3.編譯優化帶來的有序性問題

編譯器爲了優化性能,有時候會改變程序中語句的先後順序。例如程序中:“a=6;b=7;”編譯器優化後可能變成“b=7;a=6";
最典型的是雙重檢查鎖定的單例模式。

private static Singleton instace;   
  
public static Singleton getInstance(){   
    //第一次null檢查     
    if(instance == null){            
        synchronized(Singleton.class) { //1     
            //第二次null檢查       
            if(instance == null){ //2  
                instance = new Singleton();//3  
            }  
        }           
    }  
    return instance;        
}

上面這段代碼看似無懈可擊,但是現實中有可能報錯。
singleton = new Singleton();這段代碼其實是分爲三步:
分配內存空間。(1)
初始化對象。(2)
將 singleton 對象指向分配的內存地址。(3)
但是編譯器優化指令重排之後,執行順序變成了:
1.分配內存空間。(1)
2.將 singleton 對象指向分配的內存地址。(3)
3.初始化對象。(2)
第3步在第2步之前被執行,下一個線程拿到的單例對象是還沒有初始化的,以致於報空指針異常。
對singleton 使用volatile修飾,就可以解決上述的問題。
使用volatile修飾的變量,它實現的作用是,對於這個變量的讀寫,不能使用CPU緩存必須從內存中寫入或讀出。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章