Java內存模型與線程

參考

《深入理解java虛擬機-jvm高級特性與最佳實踐》第12章Java內存模型與線程
《java併發編程的藝術》第3章 java內存模型

服務性能好壞的高低指標:

TPS Transactions Per Seconds每秒處理事務數
代表一秒內服務端平均能相應的請求總數

物理計算機的併發問題
複雜性來源是 絕大部分的運算任務都不可能只靠處理器計算就能完成,處理器至少要與內存交互,如讀取運算數據,存儲運算結果等,這個io是很難消除的。
基於內存交互的複雜性,所以引入新的基於 高速緩存的存儲交互 解決了處理器與內存的速度矛盾,但是帶來新的問題: 緩存一致性
爲了解決緩存一致性,引入內存模型這一概念


內存模型
    可以理解爲在特定的操作協議下,對特定的內存或高速緩存進行讀寫訪問的過程抽象

Java的內存模型
java虛擬機定義的規範,用來屏蔽各種硬件和操作系統的內存訪問差異,以實現讓java在各個平臺下都能達到一致的內存訪問效果。
如圖:
內存模型

分幾個部分: java線程,工作內存,save和load8種操作和主內存 以及變量
java的線程有自己的工作內存,工作內存中保存了該線程用到的變量的主內存副本拷貝,
所有的變量都存儲在主內存中,線程只能對自己的工作線程中的變量副本進行讀取,賦值等操作,無法直接操作主內存
主內存中存儲的變量跟java代碼中的變量不一樣
此處變量包括 實例字段,靜態字段和構成數組對象的元素不包括局部變量和方法參數,
工作內存與主內存之間通過save和load8種操作來交互,這8種操作都是原子性的

工作內存與主內存的交互操作以及相關規則:

  • lock(鎖定):作用於主內存的變量,把一個變量標識爲一條線程獨佔狀態。
  • unlock(解鎖):作用於主內存變量,把一個處於鎖定狀態的變量釋放出來,釋放後的變量纔可以被其他線程鎖定。
  • read(讀取):作用於主內存變量,把一個變量值從主內存傳輸到線程的工作內存中,以便隨後的load動作使用
  • load(載入):作用於工作內存的變量,它把read操作從主內存中得到的變量值放入工作內存的變量副本中。
  • use(使用):作用於工作內存的變量,把工作內存中的一個變量值傳遞給執行引擎,每當虛擬機遇到一個需要使用變量的值的字節碼指令時將會執行這個操作。
  • assign(賦值):作用於工作內存的變量,它把一個從執行引擎接收到的值賦值給工作內存的變量,每當虛擬機遇到一個給變量賦值的字節碼指令時執行這個操作。
  • store(存儲):作用於工作內存的變量,把工作內存中的一個變量的值傳送到主內存中,以便隨後的write的操作。
  • write(寫入):作用於主內存的變量,它把store操作從工作內存中一個變量的值傳送到主內存的變量中。

    使用的規則:

  • 不允許read和load、store和write操作之一單獨出現
  • 不允許一個線程丟棄它的最近assign的操作,即變量在工作內存中改變了之後必須同步到主內存中。
  • 不允許一個線程無原因地(沒有發生過任何assign操作)把數據從工作內存同步回主內存中。
  • 一個新的變量只能在主內存中誕生,不允許在工作內存中直接使用一個未被初始化(load或assign)的變量。即就是對一個變量實施use和store操作之前,必須先執行過了assign和load操作。
  • 一個變量在同一時刻只允許一條線程對其進行lock操作,lock和unlock必須成對出現
    如果對一個變量執行lock操作,將會清空工作內存中此變量的值,在執行引擎使用這個變量前需要重新執行load或assign操作初始化變量的值
  • 如果一個變量事先沒有被lock操作鎖定,則不允許對它執行unlock操作;也不允許去unlock一個被其他線程鎖定的變量。
  • 對一個變量執行unlock操作之前,必須先把此變量同步到主內存中(執行store和write操作)。

先行發生happens-before

先行發生是Java內存模型中定義的兩項操作之間的偏序關係, 是用來判斷數據是否存在競爭,線程是否安全的主要依據。
之前我們判斷線程安全是用競態條件來判斷,主要就是先檢查後執行,
競態條件: 基於一種可能失效的觀察結果來做判斷或者執行某個計算。

先行發生如何來判斷線程安全?

如果多個線程對同一個變量的操作並沒有明顯的先行發生關係,那麼這個變量就有可能是線程不安全的。

java內存模型中默認的先行發生關係

  • 程序次序關係 在一個線程中,按照程序代碼順序,書寫在前的操作先行發生於書寫在後面的操作,如果考慮分支,循環等結構,應該是控制流順序而不是程序代碼順序。
    指令重排序難道不會影響程序次序關係嗎?
    在同一個線程中,有如下代碼
   int i = 0;
   int j = 0;
i賦值的語句不一定發生在給j賦值的語句之前,這是由指令的重排序決定的,對於
程序次序關係 這一規則來說,由於在這個線程中無法感知到這一點,所以並不影響它的正確性
  • 管程鎖定規則 一個unlock操作先行發生於後面對同一個鎖的lock操作。
  • volatile變量規則 對一個volatile變量的寫操作先行發生於後面對於這個變量的讀操作
    volatile修飾的變量不一定是線程安全的,所以在多線程環境中對於當前變量的寫操作不一定先行於其他線程對這個變量的讀操作。所以這個規則適用於當前線程。
    如:
    public class VolatileDemo {
    private volatile int race = 0;

    public int getRace() {
        return race;
    }

    public void setRace() {
        this.race++;
    }

    public void test1(){
        try {
            for (int i = 0; i < 100; i++) {
                /**如果是2的倍數,對volatile進行寫操作*/
                if(i % 2 == 0){
                    new Thread(() -> setRace()).start();
                }else{
                    /**讀操作*/
                    new Thread(() -> getRace()).start();
                }
            }
            System.out.println(getRace());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public void test2(){
        getRace();
        for (int i = 0; i < 100; i++) {
            setRace();
        }
        System.out.println(getRace());
    }

    public static void main(String[] args) {
        VolatileDemo volatileDemo = new VolatileDemo();
        try {
            volatileDemo.test2();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
test1的結果 : 16
test2的結果: 100
  • 線程啓動規則 thread的start方法先行發生於此線程的每一個動作
  • 線程終止規則 線程中的所有操作都先行發生於對此線程的終止檢測。
  • 線程中斷規則 對線程interrupt方法的調用先行發生於被中斷線程的代碼檢測到中斷事件的發生
  • 對象終結規則 一個對象的初始化完成先行發生於他的finalize方法的開始
  • 傳遞性 如果a先行於b,b先行於c,那麼a肯定先行於c
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章