天天看點

JUC(8)Java記憶體模型-JMM和Volatile關鍵字

文章目錄

  • ​​一、JMM​​
  • ​​1.1什麼是JMM​​
  • ​​1.2記憶體劃分​​
  • ​​1.3記憶體互動操作​​
  • ​​二、Volatile​​
  • ​​2.1 驗證 保證可見性​​
  • ​​2.2 驗證 不保證原子性​​
  • ​​2.3 驗證 禁止指令重排(有序性)​​
  • ​​2.4小結​​

一、JMM

  • JMM (java memory model) 即為JAVA 記憶體模型 ,不存在的東西,是一個概念,也是一個約定!
  • 關于JMM的一些同步的約定:
  • 1、線程解鎖前,必須把共享變量立刻刷回主存;
  • 2、線程加鎖前,必須讀取主存中的最新值到工作記憶體中;
  • 3、加鎖和解鎖是同一把鎖;

1.1什麼是JMM

  • JMM即為JAVA 記憶體模型(java memory model)。因為在不同的硬體生産商和不同的作業系統下,記憶體的通路邏輯有一定的差異,結果就是當你的代碼在某個系統環境下運作良好,并且線程安全,但是換了個系統就出現各種問題。Java記憶體模型,就是為了屏蔽系統和硬體的差異,讓一套代碼在不同平台下能到達相同的通路結果。JMM從java 5開始的JSR-133釋出後,已經成熟和完善起來。

1.2記憶體劃分

  • JMM規定了記憶體主要劃分為主記憶體和工作記憶體兩種。此處的主記憶體和工作記憶體跟JVM記憶體劃分(堆、棧、方法區)是在不同的層次上進行的,如果非要對應起來,主記憶體對應的是Java堆中的對象執行個體部分,工作記憶體對應的是棧中的部分區域,從更底層的來說,主記憶體對應的是硬體的實體記憶體,工作記憶體對應的是寄存器和高速緩存。
JUC(8)Java記憶體模型-JMM和Volatile關鍵字
  • JVM在設計時候考慮到,如果JAVA線程每次讀取和寫入變量都直接操作主記憶體,對性能影響比較大,是以每條線程擁有各自的工作記憶體,工作記憶體中的變量是主記憶體中的一份拷貝,線程對變量的讀取和寫入,直接在工作記憶體中操作,而不能直接去操作主記憶體中的變量。但是這樣就會出現一個問題,當一個線程修改了自己工作記憶體中變量,對其他線程是不可見的,會導緻線程不安全的問題。因為JMM制定了一套标準來保證開發者在編寫多線程程式的時候,能夠控制什麼時候記憶體會被同步給其他線程。
  • JUC(8)Java記憶體模型-JMM和Volatile關鍵字

遇到問題:程式不知道主存中的值已經被修改過了!;

1.3記憶體互動操作

  • 記憶體互動操作有8種,虛拟機實作必須保證每一個操作都是原子的,不可再分的(對于double和long類型的變量來說,load、store、read和write操作在某些平台上允許例外)
  • lock(鎖定):作用于主記憶體的變量,把一個變量辨別為線程獨占狀态
  • unlock(解鎖):作用于主記憶體的變量,它把一個處于鎖定狀态的變量釋放出來,釋放後的變量才可以被其他線程鎖定
  • read(讀取):作用于主記憶體變量,它把一個變量的值從主記憶體傳輸到線程的工作記憶體中,以便随後的load動作使用
  • load(載入):作用于工作記憶體的變量,它把read操作從主存中變量放入工作記憶體中
  • use(使用):作用于工作記憶體中的變量,它把工作記憶體中的變量傳輸給執行引擎,每當虛拟機遇到一個需要使用到變量的值,就會使用到這個指令
  • assign(指派):作用于工作記憶體中的變量,它把一個從執行引擎中接受到的值放入工作記憶體的變量副本中
  • store(存儲):作用于主記憶體中的變量,它把一個從工作記憶體中一個變量的值傳送到主記憶體中,以便後續的write使用
  • write(寫入):作用于主記憶體中的變量,它把store操作從工作記憶體中得到的變量的值放入主記憶體的變量中
  • JMM對這八種指令的使用,制定了如下規則:
  • 不允許read和load、store和write操作之一單獨出現。即使用了read必須load,使用了store必須write
  • 不允許線程丢棄他最近的assign操作,即工作變量的資料改變了之後,必須告知主存
  • 不允許一個線程将沒有assign的資料從工作記憶體同步回主記憶體
  • 一個新的變量必須在主記憶體中誕生,不允許工作記憶體直接使用一個未被初始化的變量。就是怼變量實施use、store操作之前,必須經過assign和load操作
  • 一個變量同一時間隻有一個線程能對其進行lock。多次lock後,必須執行相同次數的unlock才能解鎖
  • 如果對一個變量進行lock操作,會清空所有工作記憶體中此變量的值,在執行引擎使用這個變量前,必須重新load或assign操作初始化變量的值
  • 如果一個變量沒有被lock,就不能對其進行unlock操作。也不能unlock一個被其他線程鎖住的變量
  • 對一個變量進行unlock操作之前,必須把此變量同步回主記憶體

二、Volatile

  • Volatile是 Java 虛拟機提供輕量級的同步機制,他的三大特性:
  • 1、保證可見性 (這就要涉及JMM)
  • 2、不保證原子性
  • 3、禁止指令重排

下面我們來驗證三個特性

2.1 驗證 保證可見性

package com.wlw.Test_Volatile;
import java.util.concurrent.TimeUnit;

public class JMMDemo {
    /**
     * 這個程式一共有兩個線程: main線程 與 我們自己寫的一個A線程
     * A線程中對變量num 進行判斷,如果為0,就一直循環,而main線程中将num 指派為1,此時我們沒有對變量num加volatile關鍵字,A線程一直循環下去
     *
     * 如果我們對變量num加volatile關鍵字,就保證了可見性:當main線程中對num進行修改,A線程可以看見修改後的值,進而進行相關操作
     */
    private volatile static int num = 0;

    public static void main(String[] args) {

        new Thread(()->{
            while (num == 0){

            }
        },"A").start();

        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        num = 1;
        System.out.println(num);
    }
}      

2.2 驗證 不保證原子性

  • 原子性:不可分割;
  • 線程A在執行任務的時候,不能被打擾的,也不能被分割的,要麼同時成功,要麼同時失敗。
package com.wlw.Test_Volatile;

//volatile 不保證原子性

/**
 * 一共20個線程,每個線程調用1000次add()方法,理論上num最後為20000
 * 但是多線程操作,會出現同一時刻多個線程對num進行操作(因為在位元組碼檔案中num++ 這個操作被分為三步才執行完,不是原子操作),是以最後的值小于2萬
 *
 * ,加上volatile關鍵字之後,最後num結果依然小于20000,是以驗證了  volatile 不保證原子性。
 * 但是如果我們對add()方法 加上synchronized 或者 lock鎖,是一定可以保證結果為2萬的,
 * 但是問題是如果不加lock和synchronized (更耗費資源) ,怎麼樣保證原子性?
 */
public class VolatileDemo02 {

    private volatile static int num = 0;

    public static void add(){
        num++;
    }

    public static void main(String[] args) {
        // 一共20個線程,每個線程調用1000次add()方法,理論上num最後為20000
        for (int i = 1; i <= 20; i++) {
            new Thread(()->{
                for (int j = 1; j < 1000; j++) {
                    add();
                }
                add();
            }).start();
        }

        while (Thread.activeCount() > 2){
            Thread.yield();
        }
        System.out.println(Thread.currentThread().getName() + "==>" + num);
    }
}      
  • 問題:如果我們對add()方法 加上synchronized 或者 lock鎖,是一定可以保證結果為2萬的,但是問題是如果不加lock和synchronized (更耗費資源) ,怎麼樣保證原子性?
  • 解決:使用 java.util.concurrent.atomic(原子包) 包下的原子類
  • 這些類的底層都直接和作業系統挂鈎!是在記憶體中修改值。需要用到Unsafe類,而Unsafe類是一個很特殊的存在;(在21.CAS中介紹)
JUC(8)Java記憶體模型-JMM和Volatile關鍵字
package com.wlw.Test_Volatile;

//volatile 不保證原子性

import java.util.concurrent.atomic.AtomicInteger;

/**
 * 一共20個線程,每個線程調用1000次add()方法,理論上num最後為20000
 * 但是多線程操作,會出現同一時刻多個線程對num進行操作(因為在位元組碼檔案中num++ 這個操作被分為三步才執行完,不是原子操作),是以最後的值小于2萬
 * ,加上volatile關鍵字之後,最後num結果依然小于20000,是以 驗證了  volatile 不保證原子性。
 * 但是如果我們對add()方法 加上synchronized 或者 lock鎖,是一定可以保證結果為2萬的,
 * 但是問題是如果不加lock和synchronized (更耗費資源) ,怎麼樣保證原子性?
 *   解決辦法:使用 java.util.concurrent.atomic(原子包) 包下的原子類
 */
public class VolatileDemo02 {

    //private static int num = 0;
    //使用原子類AtomicInteger 替換int
    private volatile static AtomicInteger num = new AtomicInteger();

    public static void add(){
        //num++;
        num.getAndIncrement();//原子類的加1操作 , 裡面是調用的是native方法, 用的是底層的CAS(cpu的并發原語,效率極高)
    }

    public static void main(String[] args) {
        // 一共20個線程,每個線程調用1000次add()方法,理論上num最後為20000
        for (int i = 1; i <= 20; i++) {
            new Thread(()->{
                for (int j = 1; j < 1000; j++) {
                    add();
                }
                add();
            }).start();
        }

        while (Thread.activeCount() > 2){
            Thread.yield();
        }
        System.out.println(Thread.currentThread().getName() + "==>" + num);
    }
}      

2.3 驗證 禁止指令重排(有序性)

  • 什麼是指令重排?
  • 我們寫的程式,計算機并不是按照我們自己寫的那樣去執行的
  • 源代碼–>編譯器優化重排–>指令并行也可能會重排–>記憶體系統也會重排–>執行
  • 系統處理器在進行指令重排的時候,會考慮資料之間的依賴性!并不會随意地去排
int x=1; //1
int y=2; //2
x=x+5;   //3
y=x*x;   //4

//我們期望的執行順序是 1_2_3_4  指令重排後,可能執行的順序會變成2134 1324
//可不可能是 4123? 不可能的      
  • 看一個例子:

    可能造成的影響結果:前提:a b x y這四個值 預設都是0

線程A 線程B
x=a y=b
b=1 a=2

我們期望的 正常的結果: x = 0; y =0;

線程A 線程B
b=1 a=2
x=a y=b
  • volatile可以避免指令重排:是因為volatile中會加一道記憶體的屏障,這個記憶體屏障可以保證在這個屏障中的指令順序。
  • 記憶體屏障,是CPU指令。作用:
  • 1、保證特定的操作的執行順序;
  • 2、可以保證某些變量的記憶體可見性(利用這些特性,就可以保證volatile實作的可見性)

2.4小結

  • Volatile是可以保證可見性,不能保證原子性,由于記憶體屏障,可以保證避免指令重排的現象産生!
  • Volatile 記憶體屏障在哪使用最多:在單例模式中使用最多(餓漢式,DCL懶漢式中用到了Volatile),連結為: