關於 volatile 你知多少???

談談你對 volatile 的理解

1、volatile 是 Java 虛擬機提供的輕量級的同步機制

  • 保證可見性
  • 不保證原子性
  • 禁止指令重排

2、談談JMM(Java 內存模型)

JMM(Java內存模型Java Memory Model,簡稱JMM)本身是一種抽象的概念並不真實存在,它描述的是一組規則或規範,通過這組規範定義了程序中各個變量(包括實例字段,靜態字段和構成數組對象的元素)的訪問方式。

JMM關於同步的規定:
1、線程解鎖前,必須把共享變量的值刷新回主內存
2、線程加鎖前,必須讀取主內存的最新值到自己的工作內存
3、加鎖解鎖是同一把鎖

由於JVM運行程序的實體是線程,而每個線程創建時JVM都會爲其創建一個工作內存(有些地方稱爲棧空間),工作內存是每個線程的私有數據區域,而Java內存模型中規定所有變量都存儲在主內存,主內存是共享內存區域,所有線程都可以訪問,但線程對變量的操作(讀取賦值等)必須在工作內存中運行,首先要將變量從主內存拷貝的自己的工作內存空間,然後對變量進行操作,操作完成後再將變量寫回主內存,不能直接操作主內存中的變量,各個線程中的工作內存中存儲着主內存中的變量副本拷貝,因此不同的線程間無法訪問對方的工作內存,線程間的通信(傳值)必須通過主內存來完成,其簡要訪問過程如下圖:
在這裏插入圖片描述

2.1、可見性

通過前面對JMM的介紹,我們知道
各個線程對主內存中共享變量的操作都是各個線程各自拷貝到自己的工作內存進行操作後再寫回到主內存中的。

這就可能存在一個線程AAA修改了共享變量X的值但還未寫回主內存時,另外一個線程BBB又對主內存中
同一個共享變量X進行操作,但此時A線程工作內存中共享變量X對線程B來說並不可見,
這種工作內存與主內存同步延遲現象就造成了可見性問題

2.2、原子性

原子性指的是什麼意思?
不可分割, 完整性, 也即某個線程正在做某個具體業務時, 中間不可以被加塞或者被分割. 需要整體完整
要麼同時成功, 要麼同時失敗

number++ 在多線程下是非線程安全的,如何不加 synchronized解決?

在這裏插入圖片描述

2.3、VolatileDemo代碼演示可見性+原子性代碼

package com.brian.interview.study.thread;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * Copyright (c) 2020 ZJU All Rights Reserved
 * <p>
 * Project: JavaSomeDemo
 * Package: com.brian.interview.study.thread
 * Version: 1.0
 * <p>
 * Created by Brian on 2020/2/10 20:50
 */
class MyData {  // MyData.java ===> MyData.class ===> JVM字節碼
    volatile int number = 0;

    public void addTo60() {
        this.number = 60;
    }

    // 請注意, 此時 number 前面是加了 volatile 關鍵字修飾的, volatile 不保證原子性
    public void addPlusPlus(){
        number++;
    }

    AtomicInteger atomicInteger = new AtomicInteger();
    public void addAtomic(){
        atomicInteger.getAndIncrement();
    }
}

/**
 * 1、驗證 volatile 的可見性
 *   1.1 假如 int number=0; number 變量之前根本沒有添加 volatile 關鍵字修飾, 沒有可見性
 *   1.2 添加了 volatile, 可以解決可見性問題
 *
 * 2、驗證 volatile 不保證原子性
 *   2.1 原子性指的是什麼意思?
 *       不可分割, 完整性, 也即某個線程正在做某個具體業務時, 中間不可以被加塞或者被分割. 需要整體完整
 *       要麼同時成功, 要麼同時失敗
 *
 *   2.2 volatile 不保證原子性的案例演示
 *
 *   2.3 why
 *
 *   2.4 如何解決原子性?
 *     * 加 synchronized
 *     * 使用我們的 JUC 下 AtomicInteger
 */
public class VolatileDemo {
    public static void main(String[] args) {
        MyData myData = new MyData();
        for (int i = 1; i <= 20; i++) {
            new Thread(() -> {
                for (int j = 1; j <= 1000; j++) {
                    myData.addPlusPlus();
                    myData.addAtomic();
                }
            }, String.valueOf(i)).start();
        }

        // 需要等待上面20個線程都全部計算完成, 再用main線程取得最終的結果值看是多少?
        while (Thread.activeCount()>2){
            Thread.yield();
        }
        System.out.println(Thread.currentThread().getName() + "\t int type, finally number value: " + myData.number);
        System.out.println(Thread.currentThread().getName() + "\t AtomicInteger type, finally number value: " + myData.atomicInteger);
    }

    // volatile 可以保證可見性, 及時通知其它線程, 主物理內存的值已經被修改
    public static void seeOkByVolatile() {
        MyData myData = new MyData();  // 資源類

        new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + "\t come in");
            try {
                TimeUnit.SECONDS.sleep(3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            myData.addTo60();
            System.out.println(Thread.currentThread().getName() + "\t update number value: " + myData.number);
        }, "AAA").start();

        // 第2個線程就是我們的main線程
        while (myData.number == 0){
            // main 線程就一直在這裏等待循環,直到 number 值不再等於零
        }
        System.out.println(Thread.currentThread().getName() + "\t mission is over, main get number value: " + myData.number);
    }
}

2.4、有序性

計算機在執行程序時,爲了提高性能,編譯器和處理器常常會對指令做重排,一般分以下3種
在這裏插入圖片描述
單線程環境裏面確保程序最終執行結果和代碼順序執行的結果一致。

處理器在進行重排序時必須要考慮指令之間的數據依賴性

多線程環境中線程交替執行,由於編程器優化重排的存在,兩個線程中使用的變量能否保證一致性是無法確定的,結果無法預測

重排1
public void mySort(){
    int x = 11;  // 語句1
    int y = 12;  // 語句2
    x = x + 5;   // 語句3
    y = x * x;   // 語句4
}
  • 1234
  • 2134
  • 1324
問題:請問語句4可以重排後變成第一條嗎?
  • 答:不可以,處理器在進行重排序時必須要考慮指令之間的數據依賴性
重排2

int a, b, x, y=0;

線程 1 線程 2
x=a; y=b;
b=1; a=2;
 
x=0, y=0

如果編譯器對這段程序代碼執行重排優化後,可能出現下列情況

線程 1 線程 2
b=1; a=2;
x=a; y=b;
 
x=2, y=1

這也就說明在多線程環境下,由於編譯器優化重排的存在,兩個線程中使用的變量能否保證一致性是無法確定的。

重排2–案例
package com.brian.interview.study.thread;

/**
 * Copyright (c) 2020 ZJU All Rights Reserved
 * <p>
 * Project: JavaSomeDemo
 * Package: com.brian.interview.study.thread
 * Version: 1.0
 * <p>
 * Created by Brian on 2020/2/10 23:18
 */
public class ReSortSeqDemo {
    int a = 0;
    boolean flag = false;

    public void method1() {
        a = 1;        // 語句1
        flag = true;  // 語句2
    }

    // 多線程環境中線程交替執行,由於編程器優化重排的存在,
    // 兩個線程中使用的變量能否保證一致性是無法確定的,結果無法預測
    public void method2() {
        if (flag) {
            a += 5;   // 語句3
            System.out.println("*********retValue: " + a);
        }
    }
}
禁止指令重排小總結

volatile 實現禁止指令重排優化,從而避免多線程環境下程序出現亂序執行的現象

先了解一個概念,內存屏障(Memory Barrier)又稱內存柵欄,是一個CPU指令,它的作用有兩個:
一是保證特定操作的執行順序,
二是保證某些變量的內存可見性(利用該特性實現 volatile 的內存可見性)。
由於編譯器和處理器都能執行指令重排優化。如果在指令間插入一條Memory Barrier則會告訴編譯器和CPU,不管什麼指令都不能和這條Memory Barrier指令重排序,也就是說通過插入內存屏障禁止在內存屏障前後的指令執行重新排序優化。內存屏障另外一個作用是強制刷出各種CPU的緩存數據,因此任何CPU上的線程都能讀取到這些數據的最新版本。
在這裏插入圖片描述

總結: 可見性、原子性、有序性 線程安全性獲得保證

工作內存與主內存同步延遲現象導致的可見性問題
可以使用 synchronized 或 volatile 關鍵字解決,它們都可以使一個線程修改後的變量立即對其他線程可見

對於指令重排導致的可見性問題和有序性問題
可以利用 volatile 關鍵字解決,因爲 volatile 的另外一個作用就是禁止重排序優化。

3、在哪些地方用到過 volatile?

3.1、單例模式 DCL 代碼

package com.brian.interview.study.thread;

/**
 * Copyright (c) 2020 ZJU All Rights Reserved
 * <p>
 * Project: JavaSomeDemo
 * Package: com.brian.interview.study.thread
 * Version: 1.0
 * <p>
 * Created by Brian on 2020/2/10 23:58
 */
public class SingletonDemo {

    public static volatile SingletonDemo instance = null;

    private SingletonDemo() {
        System.out.println(Thread.currentThread().getName() + "\t 我是構造方法SingletonDemo()");
    }

    // DCL (Double Check Lock 雙端檢鎖機制)
    public static SingletonDemo getInstance() {
        if (instance == null) {
            synchronized (SingletonDemo.class) {
                if (instance == null) {
                    instance = new SingletonDemo();
                }
            }
        }
        return instance;
    }

    public static void main(String[] args) {
        // 單線程(main線程的操作動作......)
//        System.out.println(SingletonDemo.getInstance() == SingletonDemo.getInstance());
//        System.out.println(SingletonDemo.getInstance() == SingletonDemo.getInstance());
//        System.out.println(SingletonDemo.getInstance() == SingletonDemo.getInstance());
//
//        System.out.println();
//        System.out.println();
//        System.out.println();

        // 併發多線程後, 情況發生了很大的變化
        for (int i = 1; i <= 10; i++) {
            new Thread(() -> {
                SingletonDemo.getInstance();
            }, String.valueOf(i)).start();
        }
    }
}

3.2、單例模式 volatile 分析

DCL(雙端檢鎖)機制不一定線程安全,原因是有指令重排序的存在,加入 volatile 可以禁止指令重排

原因在於某一個線程執行到第一次檢測,讀取到的 instance 不爲 null 時,instance 的引用對象可能沒用完成初始化

instance = new SingletonDemo(); 可以分爲以下3步完成(僞代碼)

memory = allocate();  // 1、分配對象內存空間
instance(memory);    // 2、初始化對象
instance = memory;  // 3、設置 instance 指向剛分配的內存地址,此時 instance!=null

步驟2和步驟3不存在數據依賴關係,而且無論重排前還是重排後程序的執行結果在單線程中並沒有改變,因此這種重排優化是允許的。

memory = allocate();  // 1、分配對象內存空間
instance = memory;  // 3、設置 instance 指向剛分配的內存地址,此時 instance!=null,但是對象還沒有初始化完成!
instance(memory);    // 2、初始化對象

但是指令重排只會保證串行語義的執行的一致性(單線程),但並不會關心多線程間的語義一致性。
所以當一條線程訪問 instance 不爲 null 時,由於 instance 實例未必已初始化完成,也就造成了線程安全問題。

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