volatile 關鍵字原理解析

今天,來談談 Java 并發(fā)編程中的一個基礎知識點:volatile 關鍵字
本篇文章主要從可見性,原子性和有序性進行講解

  • Java的內層模型
  • happen-before
  • 可見性
  • 有序性

一. 主存與工作內存

說 volatile 之前,先來聊聊 Java 的內存模型。

在 Java 內存模型中,規(guī)定了所有的變量都是存儲在主內存當中,而每個線程都有屬于自己的工作內存。線程的工作內存保存了被該內存使用到的變量的主內存副本拷貝,線程對變量的所有操作(讀取,賦值等)都必須在工作內存中進行,而不能直接對主存進行操作。并且每個線程不能訪問其他線程的工作內存。

對于單線程的程序,這樣的規(guī)定沒有任何影響;但是對于多線程的程序,便可能導致,某個線程已經改變了主內存中的變量,而另一個線程還在使用其工作內存中的變量,因此造成了數(shù)據(jù)的不一致。

Java內層模型

二. 可見性

volatile 可以保證數(shù)據(jù)的可見性,前面說到對于多線程的程序可能會造成數(shù)據(jù)不一致,但是當一個變量加上 volatile 之后,便可以保證,其他線程讀取到的該變量都是最新值。

這是因為每當對該變量進行寫操作時,都會使得其他線程工作變量中的該變量的拷貝失效,而迫使線程們都重新去主內存讀取

我們來看看實例:

public class TestThread extends Thread {
    private volatile boolean isRunning = true;

    public void setRunning(boolean running) {
        isRunning = running;
    }

    @Override
    public void run() {
        int i = 1;
        while (isRunning) {
            i++;
        }
        System.out.println(i);
    }
}

public class Test {
    public static void main(String[] args) throws InterruptedException {
        TestThread thread = new TestThread ();
        thread.start();
        Thread.sleep(3000);
        thread.setRunning(false);
    }
}

當 isRunning 變量沒有添加 volatile 變量時,該程序會發(fā)生死循環(huán),因為setRunning(false)并沒有影響到 thread 所在線程的工作內存(這時該線程看到的值仍然是 true)

當我們?yōu)樽兞刻砑由?volatile 之后,setRunning(false)執(zhí)行完畢,thread 所在線程的工作內存的變量拷貝便就此作廢,必須去主內存獲取最新的值,死循環(huán)也因此不會再發(fā)生了

值得注意的是,當我們在循環(huán)中添加了打印語句,或者 sleep 方法等,這時無論有沒有 volatile,都會停止循環(huán),如:

 while (isRunning) {
    i++;
    System.out.println(i);
}

這是因為,JVM 會盡力保證內存的可見性,原本的代碼中,程序一直處于死循環(huán),這時 JVM 沒有辦法強制要求 CPU 分出時間去保證可見性;但是當加上打印語句之后,CPU 便會分出時間去處理這件事情,并保證了可見性;但是,與之不同的是,volatile 是強制保證可見性的。

三. 原子性

volatile 沒有辦法保證操作的原子性的

直接上代碼:

public class AtomicTest {
    private static volatile int race = 0;

    private static void increase() {
        race++;
    }

    private static final int THREADS_COUNT = 20;

    public static void main(String[] args) {
        Thread[] threads = new Thread[THREADS_COUNT];
        for (int i = 0; i < 10; i++) {
            threads[i] = new Thread(() -> {
                for (int j = 0; j < 1000; j++) {
                    increase();
                }
            });
            threads[i].start();
        }
        //等待所有累加線程都結束
        while (Thread.activeCount() > 1) {
            Thread.yield();
        }
        System.out.println(race);
    }
}

這段代碼摘自《深入理解 Java 虛擬機》12 章,當 race 沒有 volatile 關鍵字的加持時,最終的打印結果經常會小于 10000,而有了 volatile,這段程序變不再出現(xiàn)這種情況。

假設兩個線程 1 和 2,它們倆先后讀取了 race 的值(初始值為 0),由于它們都還沒有進行寫操作,因此兩個線程這時看到的值都是 0,因此便使得之后兩次自增操作的結果是 1,而不是 2

image

剛剛說到 volatile 變量在進行寫操作的時候,會讓其他線程對應的工作內存中的拷貝失效,使得需要直接去主存中讀取變量,而上例中線程 1 在進行寫操作之前,線程 2 便已經執(zhí)行了讀操作,因此沒辦法影響線程 2 的讀取,因此也不會更新為最新的數(shù)據(jù)了

四. 有序性

volatile 可以在一定程度上禁止指令重排序

重排序

重排序是指編譯器和處理器為了優(yōu)化程序性能而對指令序列進行重新排序的一種手段。在單線程程序中,對存在控制依賴的操作重排序,不會改變執(zhí)行結果(這也是as-if-serial語義允許對存在控制依賴的操作做重排序的原因);但在多線程程序中,對存在控制依賴的操作重排序,可能會改變程序的執(zhí)行結果。

//x、y為非volatile變量
//flag為volatile變量

x = 2;        //語句1
y = 0;        //語句2
flag = true;  //語句3
x = 4;         //語句4
y = -1;       //語句5

flag 變量添加上 volatile 關鍵字以后,語句 1,2 不會排在 3 的后面執(zhí)行,當然 4,5 也不會在 3 的前面執(zhí)行

但是 1 和 2, 3 和 4 之間的順序沒辦法干預,這也是我們說“一定程度改變”的原因

上個例子:

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

//線程2:
while(!inited){
  sleep()
}
doSomethingwithconfig(context); //出錯,context 可能還沒有初始化

面對這樣的例子的時候,如果 inited 是非 volatile 變量,那么因為重排序的關系,有可能出錯;但是加上 volatile 后便不用擔心了

happens-before

      如果一個操作執(zhí)行的結果需要對另一個操作可見,那么這兩個操作之間必須要存在happens-before關系。這里提到的兩個操作既可以是在一個線程之內,也可以是在不同線程之間。
     對happens-before關系的具體定義如下。
    ① 如果一個操作happens-before另一個操作,那么第一個操作的執(zhí)行結果將對第二個操作可見,而且第一個操作的執(zhí)行順序排在第二個操作之前。
    ②兩個操作之間存在happens-before關系,并不意味著Java平臺的具體實現(xiàn)必須要按照 happens-before關系指定的順序來執(zhí)行。如果重排序之后的執(zhí)行結果,與按happens-before關系來執(zhí)行的結果一致,那么這種重排序并不非法(也就是說,JMM允許這種重排序)。

      上面的①是JMM對程序員的承諾。從程序員的角度來說,可以這樣理解happens-before關系:如果A happens-before B,那么Java內存模型將向程序員保證——A操作的結果將對B可見,且A的執(zhí)行順序排在B之前。注意,這只是Java內存模型向程序員做出的保證!上面的②是JMM對編譯器和處理器重排序的約束原則。正如前面所言,其實是在遵循一個基本原則:只要不改變程序的執(zhí)行結果(指的是單線程程序和正確同步的多線程程序),編譯器和處理器怎么優(yōu)化都行。因此,happens-before關系本質上和as-if-serial語義是一回事。

        as-if-serial語義保證單線程內程序的執(zhí)行結果不被改變,happens-before關系保證正確同步的多線程程序的執(zhí)行結果不被改變。
        as-if-serial語義給編寫單線程程序的程序員創(chuàng)造了一個幻境:單線程程序是按程序的順序來執(zhí)行的。happens-before關系給編寫正確同步的多線程程序的程序員創(chuàng)造了一個幻境:正確同步的多線程程序是按happens-before指定的順序來執(zhí)行的。
        as-if-serial語義和happens-before這么做的目的,都是為了在不改變程序執(zhí)行結果的前提下,盡可能地提高程序執(zhí)行的并行度。

   happens-before規(guī)則如下:

    程序順序規(guī)則:一個線程中的每個操作,happens-before于該線程中的任意后續(xù)操作。
    監(jiān)視器鎖規(guī)則:對一個鎖的解鎖,happens-before于隨后對這個鎖的加鎖。
    volatile變量規(guī)則:對一個volatile域的寫,happens-before于任意后續(xù)對這個volatile域的讀。
    傳遞性:如果A happens-before B,且B happens-before C,那么A happens-before C。
    start()規(guī)則:如果線程A執(zhí)行操作ThreadB.start()(啟動線程B),那么A線程的ThreadB.start()操作happens-before于線程B中的任意操作。
    join()規(guī)則:如果線程A執(zhí)行操作ThreadB.join()并成功返回,那么線程B中的任意操作happens-before于線程A從ThreadB.join()操作成功返回。

五. 使用場景

1. 狀態(tài)變量:

比如上面給出的可見性的代碼例子

while (isRunning) {
      i++;
}

對于這種用于標記狀態(tài)的變量,volatile 是非常好用的

2. 雙重檢驗:

最經典的就是單例模式的雙重檢驗實現(xiàn),如果忘了的剛好復習一下:

public class Singleton {
    private volatile static Singleton singleton;

    private Singleton() {
    }

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

這里的 volatile,是為了保證 singleton = new Singleton();操作的有序性,因為 singleton = new Singleton(); 并不是原子操作,做了 3 件事

  1. 給 singleton 分配內存
  2. 調用 Singleton 的構造函數(shù)來初始化成員變量
  3. 將 singleton 對象指向分配的內存空間(執(zhí)行完這步 singleton 就為非 null 了)

但是由于重排序的原因,1-2-3 的順序可能變成 1-3-2,如果是后者,在 singleton 變成非 null 時(即第三步),如果第二個線程開始進入第一個判斷 if (singleton == null),那么便會直接返回 true,然而事實上 singleton 還沒有完成初始化

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

友情鏈接更多精彩內容