JMM簡單學習

JMM學習

學習資料:

b站諸葛老師-JMM篇

一、系統CPU和主內存交互圖:

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-LFfRxagn-1593677912974)(E:\typoraPic\image-20200701200952976.png)]

二、JMM模型

Java線程內存模型跟cpu緩存模型類似,是基於cpu緩存模型來建立的,java線程內存模型是標準化的,屏蔽掉了底層不同計算機的區別

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-cUlphejS-1593677912982)(E:\typoraPic\image-20200701203102180.png)]

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-stMJehgC-1593677912989)(E:\typoraPic\image-20200701213140382.png)]

注意:每個線程操作共享變量操作的是複製主內存的副本,當一個線程修改了自己工作內存中變量,對其他線程是不可見的,會導致線程不安全的問題。下面代碼演示這種問題:

代碼測試:

package com.lxf.volatileT;

import java.util.concurrent.TimeUnit;

/**
 * 兩個線程訪問同一個共享變量,一個線程修改完共享變量後,另一個線程對這個共享變量是不可見的
 */
public class JMMDemo {
    private  static   boolean flag=true;

    public static  void main(String[] args) {

        new Thread(()->{//線程1
            while (flag){

            }
        }).start();

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

        flag=false;
        System.out.println("flag="+flag);
    }
}

結果:打印flag=true,但是線程1並未停止

解決方法:

package com.lxf.volatileT;

import java.util.concurrent.TimeUnit;

/**
 * volatile實現實現之間共享變量可見
 *volatile作用:線程之間可見、有序性(防止指令重排)、不能保證原子性
 */
public class JMMDemo {
    private  static volatile  boolean flag=true;

    public static  void main(String[] args) {

        new Thread(()->{//線程1
            while (flag){

            }
        }).start();

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

        flag=false;
        System.out.println("flag="+flag);
    }
}

在打印flag=true之後,線程1也停止了

三、JMM數據原子操作、volatile原理、volatile特性

3.1、JMM數據原子操作
  1. read(讀取):從主內存讀取數據
  2. load(載入):將主內存讀取到的數據寫入工作內存
  3. use(使用):從工作內存讀取到的數據來計算
  4. assign(賦值):將計算好的值重新賦值到工作內存中
  5. store(存儲):將工作內存數據寫入主內存
  6. write(寫入):將sotre過去的變量值賦值給主內存中的變量
  7. lock(鎖定):將主內存變量加鎖,標識爲線程獨佔狀態
  8. unlock(解鎖):將主內存變量解鎖,解鎖後其它線程可以鎖定該變量
3.2、JMM對這八種指令的使用,制定瞭如下規則:
  1. 不允許read和load、store和write操作之一單獨出現。即使用了read必須load,使用了store必須write

  2. 不允許線程丟棄他最近的assign操作,即工作變量的數據改變了之後,必須告知主存

  3. 不允許一個線程將沒有assign的數據從工作內存同步回主內存

  4. 一個新的變量必須在主內存中誕生,不允許工作內存直接使用一個未被初始化的變量。就是懟變量實施use、store操作之前,必須經過assign和load操作

  5. 一個變量同一時間只有一個線程能對其進行lock。多次lock後,必須執行相同次數的unlock才能解鎖。

  6. 如果對一個變量進行lock操作,會清空所有工作內存中此變量的值,在執行引擎使用這個變量前,必須重新load或assign操作初始化變量的值

  7. 如果一個變量沒有被lock,就不能對其進行unlock操作。也不能unlock一個被其他線程鎖住的變量。

  8. 對一個變量進行unlock操作之前,必須把此變量同步回主內存

3.3、查看底層彙編指令
  1. 下載顯示彙編代碼的插件放入jdk中
  2. java程序彙編代碼查看:-server -Xcomp -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly -XX:CompileCommand=compileonly,*VolatileVisbilityTest.prepareData
3.4、根據彙編代碼探究Volatile緩存可見性實現原理
  • 底層實現主要是通過彙編lock前綴指令,它會鎖定這塊內存區域的緩存(緩存行鎖定)並回寫到主內存。
  • IA-32架構軟件開發者手冊對lock指令的解釋:
    • 會將當前處理器緩存行的數據立即寫回到系統內存。
    • 這個寫回內存的操作會引起在其他CPU裏緩存了該內存地址的數據無效(MESI協議)

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-864yzvcV-1593677912998)(E:\typoraPic\image-20200702101909076.png)]

3.5、volatile的特性
  • 首先我們要知道併發編程的三大特性:可見性、原子性、有序性
  • Volatile保證可見性與有序性,但是不保證原子性,保證原子性需要藉助synchronized、lock這樣的鎖機制

Volatile不保證原子性代碼測試:

package com.lxf.jmm;


public class VolatileAtomicTest{
    public static volatile int num=0;

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

    public static void main(String[] args) throws InterruptedException {
        Thread[] threads=new Thread[10];
        for (int i = 0; i < threads.length; i++) {
            threads[i]=new Thread(()->{
                for (int j = 0; j < 1000; j++) {
                    increase();
                }
            });
            threads[i].start();
        }
        //等所有線程執行完了再執行main
        for (Thread thread : threads) {
            thread.join();
        }

        System.out.println("num="+num);
    }
}

結果是<=10000,多次運行就能看到。

分析原因:

num++是一組操作(假設num值初始值爲0),裏面分爲三步原子操作:讀取到num值,num值加1,將num值寫回緩存,這三步操作都有可能阻塞。假如一個線程讀取num值後阻塞,然後另一個線程執行完了:將num賦值爲1,然後這個線程繼續執行將值仍然返回爲1,這就造成了上面的問題。

參考博文:volatile爲什麼不能保證原子性

Volatile保證有序性測試(禁止指令重排)

package com.lxf.jmm;

import java.util.HashMap;
import java.util.Map;

public class VolatileSerialTest {
    static int x=0,y=0;

    public static void main(String[] args) throws InterruptedException {
        //存結果的Map集合
        Map<String,Integer> resultMap=new HashMap<>();

        for (int i = 0; i < 1000000; i++) {
            x=0;y=0;
            resultMap.clear();
            //第一個線程
            Thread one=new Thread(()->{
                int a=y;
                x=1;
                resultMap.put("a",a);
            });
            //第二個線程
            Thread two=new Thread(()->{
                int b=x;
                y=1;
                resultMap.put("b",b);
            });
            //第一個線程開啓
            one.start();
            //第二個線程開啓
            two.start();
            //第一個線程插隊
            one.join();
            //第二個線程插隊
            two.join();

            if(resultMap.get("a")==0&&resultMap.get("b")==0){
                System.out.println("===============a等於0,b也等於0================================");
                System.out.println("a=" + resultMap.get("a") + ",b=" + resultMap.get("b"));
                System.out.println("===============a等於0,b也等於0================================");
            }else if(resultMap.get("a")==1&&resultMap.get("b")==1){
                System.out.println("===============a等於1,b也等於1================================");
                System.out.println("a=" + resultMap.get("a") + ",b=" + resultMap.get("b"));
                System.out.println("===============a等於1,b也等於1================================");
            }else{
                System.out.println("a=" + resultMap.get("a") + ",b=" + resultMap.get("b"));
            }

        }
    }
}

結果可能性:

a=0,b=0

a=0,b=1

a=1,b=0

a=1,b=1

結果分析:前三個可能性我們都知道,當one線程在前結果就是a=1,b=0,當two線程在前結果就是a=0,b=1,當兩個線程同時運行:a=0,b=0。但是a=1,b=1這種可能性還是難以理解,但還是存在的(很少見,得大量運行)見下圖:

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-0WgU29NB-1593677913007)(E:\typoraPic\image-20200702155842771.png)]

因爲發生了指令重排序:

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-2IKY8EZy-1593677913011)(E:\typoraPic\image-20200702160659736.png)]

解決方法:定義x,y的時候加上volatile關鍵字就可以解決了

具體解釋看以下博文:

參考博文:
1. 併發關鍵字volatile(重排序和內存屏障)

2.Volatile禁止指令重排

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