併發編程之三個主要問題(可見性,原子性,有序性)

可見性、原子性和有序性問題

CPU、內存和IO設備隨着時代發也在不斷更新迭代,不過更新迭代的同時,他們之間的矛盾也一直存在,三者存在巨大的速度差異(CPU>內存>IO)。
操作系統的整體性能就取決於它的·短板:IO讀寫,爲此爲了平衡他們之間的速度差異,操作系統和計算機體系也提供了許多對策來平衡他們之間的速度:
1、增加CPU緩存,平衡CPU和內存的速度差異。
2、操作系統增加了進程和線程,均衡CPU和IO設備的速度差異
3、編譯程序優化指令執行次序,是緩存得到更加合理的使用
機遇和風險並存,有相應的對策,自然會引入其他問題。
問題一:引入CPU緩存帶來的可見性問題
何爲可見性:可見性就是一個線程對共享變量的修改,另一個線程能夠立即看到,即稱爲可見性
單核時代所有線程都在一顆CPU上運行,所以用的都是同一塊緩存,所以一個線程對緩存的讀寫對於另一個線程來說一定是可見的。多核時代每顆CPU都有自己的緩存,當多個線程在不同cpu上執行時,操作的是不同的CPU緩存,因此就不具備可見性了
問題二:線程切換帶來的原子性問題
一個或者多個操作在CPU執行過程中不被中斷的特性稱爲原子性,CPU能保證原子操作是針對於CPU本身指令,而非高級語言指令層面,所以很多時候我們需要在高級語言層面保證操作的原子性
問題三:編譯優化帶來的有序性問題
下面一個例子雙重檢驗創建單例單例對象public class

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

如果線程A\B同時訪問上面的方法,他們會發現instance==null,於是同時對Singleton進行加鎖,此時JVM保證只有一個線程能加鎖成功,另一個線程處於等待狀態。看起來一切都很美好,可實際不然,問題出在new操作上面:1、分配一塊內存M;2、在內存M上初始化Singleton對象;3、然後將M的地址賦值給instance變量編譯優化後會出現以下情況:1、分配一塊內存M;2、將M的地址賦值給instance變量;3、最後再在M地址上初始化Singleton對象。這樣的編譯優化後在指令2,instance就不爲null,這個時候如果線程切換到B線程,B線程剛好調用getInstance方法,因爲instance不爲null,所以直接就返回instance,但是這instance是還沒初始化的,就可能出現異常。解決方式是對instance進行volatile語義聲明(保證了不同線程對這個變量進行操作時的可見性,即一個線程修改了某個變量的值,這新值對其他線程來說是立即可見的。(實現可見性)禁止進行指令重排序。(實現有序性)volatile 只能保證對單次讀/寫的原子性。i++ 這種操作不能保證原子性。)

補充總結:cpu的緩存不位於內存中,至於系統啥時候把數據寫進緩存中,這個沒有固定時間。使用volatile這個關鍵字可以時線程A執行完後的值強制從緩存刷入到內存中,而線程B在執行時候,會強制讀取內存中的數字來更新緩存。

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