Java內存模型和併發編程

主要參考:http://www.cnblogs.com/dolphin0520/

Java內存模型(Java Memory Model,JMM)

內存模型規定:

  1. 所有的變量都是存在主存當中(類似於物理內存)
  2. 每個線程都有自己的工作內存(類似於高速緩存)
  3. 線程對變量的所有操作都必須在工作內存中進行,而不能直接對主存進行操作。
  4. 每個線程不能訪問其他線程的工作內存。

緩存一致性(Cache coherence)問題

當程序在運行過程中,會將運算需要的數據從主存複製一份到CPU的高速緩存當中,那麼CPU進行計算時就可以直接從它的高速緩存讀取數據和向其中寫入數據,當運算結束之後,再將高速緩存中的數據刷新到主存當中

在一個系統中,當許多不同的設備共享一個共同存儲器資源,在高速緩存中的數據不一致,就會產生問題。也就是說如果一個變量在多個CPU中都存在緩存(一般在多線程編程時纔會出現),那麼就可能存在緩存不一致的問題。

緩存一致性可以分爲三個層級:

  • 在進行每個寫入運算時都立刻採取措施保證數據一致性
  • 每個獨立的運算,假如它造成數據值的改變,所有進程都可以看到一致的改變結果
  • 在每次運算之後,不同的進程可能會看到不同的值(這也就是沒有一致性的行爲)

爲了解決緩存不一致性問題,通常來說有以下2種解決方法:

1.通過在總線加LOCK鎖的方式
2.通過緩存一致性協議。

最出名的就是Intel 的MESI協議,MESI協議保證了每個緩存中使用的共享變量的副本是一致的。

它核心的思想是:當CPU寫數據時,如果發現操作的變量是共享變量,即在其他CPU中也存在該變量的副本,會發出信號通知其他CPU將該變量的緩存行置爲無效狀態,因此當其他CPU需要讀取這個變量時,發現自己緩存中緩存該變量的緩存行是無效的,那麼它就會從內存重新讀取。

併發編程的三個問題:原子性問題,可見性問題,有序性問題

原子性

概念

  1. 一個操作或者多個操作,要麼全部執行,要麼就都不執行

  2. 執行的過程不會被任何因素打斷

只有簡單的讀取、賦值(而且必須是將數字賦值給某個變量,變量之間的相互賦值不是原子操作)纔是原子操作。Java內存模型只保證了基本讀取和賦值是原子性操作。

Java語言提供的保證:

通過synchronized和Lock來實現。由於synchronized和Lock能夠保證任一時刻只有一個線程執行該代碼塊,那麼自然就不存在原子性問題了,從而保證了原子性。

可見性

當多個線程訪問同一個變量時,一個線程修改了這個變量的值,其他線程能夠立即看得到修改的值。

普通的共享變量被修改之後,什麼時候被寫入主存是不確定的,當其他線程去讀取時,此時內存中可能還是原來的舊值。

Java語言提供的保證

  1. volatile關鍵字來保證可見性。當一個共享變量被volatile修飾時,它會保證修改的值會立即被更新到主存,當有其他線程需要讀取時,它會去內存中讀取新值。

  2. 通過synchronized和Lock也能夠保證可見性,synchronized和Lock能保證同一時刻只有一個線程獲取鎖然後執行同步代碼,並且在釋放鎖之前會將對變量的修改刷新到主存當中。因此可以保證可見性。

有序性

程序執行的順序按照代碼的先後順序執行。

指令重排序(Instruction Reorder)
處理器爲了提高程序運行效率,可能會對輸入代碼進行優化,它不保證程序中各個語句的執行先後順序同代碼中的順序一致,但是它會保證程序最終執行結果和代碼順序執行的結果是一致的。

處理器在進行重排序時是會考慮指令之間的數據依賴性。重排序會影響多個線程內程序執行的結果。換句話說,重排不會影響單個線程內程序執行的結果。

結論:指令重排的適用範圍是線程,它不會影響單個線程的執行,但是會影響到線程併發執行的正確性。

舉個參考資料裏的例子:
我相信,有經驗的程序員,即使不懂的指令重排,本能上也不會用兩個線程異步的執行下訴初始化操作。

//線程1:
context = loadContext();   //語句1
inited = true;             //語句2

//線程2:
while(!inited ){
    sleep()
}
doSomethingWithConfig(context);

計劃執行:在線程1裏執行語句1,初始化加載操作,完成後,標誌位inited置爲true,線程2,跳出睡眠,執行doSomethingWithConfig方法。

實際執行:由於語句1和語句2沒有數據依賴性,因此可能會被重排序。假如發生了重排序,在線程1執行過程中先執行語句2,而此是線程2會以爲初始化工作已經完成,那麼就會跳出while循環,去執行doSomethingwithconfig(context)方法,而此時context並沒有被初始化,就會導致程序出錯。

Java語言提供的保證:

通過synchronized和Lock來保證有序性,很顯然,synchronized和Lock保證每個時刻是有一個線程執行同步代碼,相當於是讓線程順序執行同步代碼,自然就保證了有序性

volatile關鍵字

一個共享變量(類的成員變量、類的靜態成員變量)被volatile修飾之後,那麼就具備了兩層語義:

1.保證了不同線程對這個變量進行操作時的可見性,即一個線程修改了某個變量的值,這新值對其他線程來說是立即可見的。

2.禁止進行指令重排序。

  1. 當程序執行到volatile變量的讀操作或者寫操作時,在其前面的操作的更改肯定全部已經進行,且結果已經對後面的操作可見;在其後面的操作肯定還沒有進行;

  2. 在進行指令優化時,不能將在對volatile變量訪問的語句放在其後面執行,也不能把volatile變量後面的語句放到其前面執行。

volatile關鍵字的使用場景

volatile關鍵字是無法替代synchronized關鍵字的,因爲volatile關鍵字無法保證操作的原子性。因此,當保證操作是原子性操作,才能保證使用volatile關鍵字的程序在併發時能夠正確執行。

  1. 狀態標記量
  2. double check。

單例的最優寫法:(線程安全)

class Singleton{

    private volatile static Singleton instance = null;

    private Singleton() {

    }

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

參考資料彙總

發佈了73 篇原創文章 · 獲贊 75 · 訪問量 15萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章