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緩存必須從內存中寫入或讀出。