Java 之 volatile 詳解

一、概念

volatile 是 Java 中的關(guān)鍵字,是一個(gè)變量修飾符,被用來修飾會被不同線程訪問和修改的變量。

二、volatile 作用

1. 可見性

可見性是指多個(gè)線程訪問同一個(gè)變量時(shí),其中一個(gè)線程修改了該變量的值,其它線程能夠立即看到修改的值。

在 Java 內(nèi)存模型中,所有的變量都存儲在主存中,同時(shí)每個(gè)線程都擁有自己的工作線程,用于提高訪問速度。線程會從主存中拷貝變量值到自己的工作內(nèi)存中,然后在自己的工作線程中操作變量,而不是直接操作主存中的變量,由于每個(gè)線程在自己的內(nèi)存中都有一個(gè)變量的拷貝,就會造成變量值不一致的問題。

如下面的代碼所示:

測試類:

class VolatileTestObj {

    private String value = null;
    private boolean hasNewValue = false;

    public void put(String value) {
        while (hasNewValue) {
            // 等待,防止重復(fù)賦值
        }
        this.value = value;
        hasNewValue = true;
    }

    public String get() {
        while (!hasNewValue) {
            // 等待,防止獲取到舊值
        }
        String value = this.value;
        hasNewValue = false;
        return value;
    }
}

測試代碼:

public class VolatileTest {

    public static void main(String... args) {
        VolatileTestObj obj = new VolatileTestObj();
        new Thread(() -> {
            while (true) {
                obj.put("time:" + System.currentTimeMillis());
            }
        }).start();
        new Thread(() -> {
            while (true) {
                System.out.println(obj.get());
            }
        }).start();
    }
}

以上測試代碼中,一個(gè)線程進(jìn)行賦值操作,另一個(gè)線程取值,運(yùn)行該測試代碼可以發(fā)現(xiàn),很容易阻塞在循環(huán)等待中。

這是因?yàn)閷懢€程寫入一個(gè)新值,同時(shí)將 hasNewValue 置為 true,但是只更新了寫線程自己工作線程的緩存值,沒有更新主存中的值。而讀線程在獲取新值是,其工作線程中的 hasNewValue 為 false,會陷入到循環(huán)等待中,即使寫線程寫了新值,讀線程也無法獲取。因?yàn)樽x線程沒有獲取都新值,寫線程的 hasNewValue 沒有被置回 false,所以寫線程也會陷入到循環(huán)等待中。因此產(chǎn)生了死鎖。

使用 volatile 關(guān)鍵字可以解決這個(gè)問題,使用 volatile 修飾的變量確保了線程不會將該變量拷貝到自己的工作線程中,所有線程對該變量的操作都是在主存中進(jìn)行的,所以 volatile 修飾的變量對所有線程可見。

使用 volatile 修飾 hasNewValue,這樣在寫線程和讀線程中都是在主存中操作 hasNewValue 的值,就不會產(chǎn)生死鎖。

2. 原子性

volatile 只保證單次讀/寫操作的原子性,對于多步操作,volatile 不能保證原子性,如下代碼所示:

測試類:

class VolatileCounter {

    private volatile int count = 0;

    public void inc() {
        count++;
    }

    public void dec() {
        count--;
    }

    public int get() {
        return count;
    }
}

測試代碼:

public class VolatileTest {

    public static void main(String... args) {
        while (true) {
            VolatileCounter counter = new VolatileCounter();
            Thread thread1 = new Thread(() -> {
                for (int i = 0; i < 50; i++) {
                    counter.inc();
                }
            });
            Thread thread2 = new Thread(() -> {
                for (int i = 0; i < 50; i++) {
                    counter.dec();
                }
            });
            thread1.start();
            thread2.start();
            try {
                thread1.join();
                thread2.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            System.out.println("counter = " + counter.get());
        }
    }
}

運(yùn)行結(jié)果:

...
counter = 0
counter = 0
counter = 0
counter = 0
counter = -21
counter = 0
counter = 0
counter = 0
counter = 0
...

從運(yùn)行結(jié)果可以看出,絕大部分情況下輸出結(jié)果為 counter = 0,但也有部分其它結(jié)果。由此可知,對于 count++; 和 count--; 這兩個(gè)操作并不具有原子性。

這是因?yàn)?count++ 是一個(gè)復(fù)合操作,包括三個(gè)部分:

  1. 讀取 count 的值;

  2. 對 count 加 1;

  3. 將 count 的值寫回內(nèi)存;

volatile 對于這三步操作是無法保證原子性的,所以會出現(xiàn)上述運(yùn)行結(jié)果。

所以,vloatile 并不能解決所有同步的問題

3. 有序性

在 Java 內(nèi)存模型中,允許編譯器和處理器對指令進(jìn)行重排序,重排序過程不會影響到單線程程序的執(zhí)行,但是會影響到多線程并發(fā)執(zhí)行的正確性。

volatile 關(guān)鍵字可以禁止指令重新排序,可以保證一定的有序性。

volatile 修飾的變量的有序性有兩層含義:

  1. 所有在 volatile 修飾的變量寫操作之前的寫操作,將會對隨后該 volatile 修飾的變量讀操作之后的語句可見。

  2. 禁止 JVM 重排序:volatile 修飾的變量的讀寫指令不能和其前后的任何指令重排序,其前后的指令可能會被重排序。

3.1 happen-before

happen-before 關(guān)系是用來判斷是否存在數(shù)據(jù)競爭、線程是否安全的主要依據(jù),也是指令重排序的依據(jù),保證了多線程下的可見性。

volatile 修飾的變量在讀寫時(shí)會建立 happen-before 關(guān)系。

如下面的測試類:

class VolatileOrder {

    int i = 0;
    volatile boolean flag = false;

    public void write() {
        i = 1; // 步驟 1
        flag = true; // 步驟 2
    }

    public String get() {
        if (flag) { // 步驟 3
            System.out.println("i = " + i); // 步驟 4
        }
    }
}

上面的代碼依據(jù) happen-before 原則(關(guān)于 happen-before 原則可自行搜索)會建立如下的關(guān)系:

  • 根據(jù) happen-before 單線程順序原則會有:步驟 1 happen-before 步驟 2、步驟 3 happen-before 步驟 4;

  • 根據(jù) happen-before 的 volatile 原則會有:步驟 2 happen-before 步驟 3;

  • 根據(jù) happen-before 的傳遞性原則會有:步驟 1 happen-before 步驟 4;

所以 步驟 1 對于 步驟 4 是可見的,即變量 i 在多個(gè)線程中具有可見性。

這也解釋了 volatile 有序性的第一層含義:所有在 volatile 修飾的變量寫操作之前的寫操作,將會對隨后該 volatile 修飾的變量讀操作之后的語句可見。

利用這個(gè)特性可以優(yōu)化變量在線程間的可見性,不需要對每個(gè)變量都用 volatile 修飾,只需要用 volatile 修飾一部分變量即可保證其它變量在多線程間也具有可見性。

3.2 禁止 JVM 重排序

對于上述代碼,如果變量 flag 沒有使用 volatile 修飾,那么步驟 1 和步驟 2 就有可能被 JVM 重排序,就無法得到上述的 happen-before 關(guān)系,所以 volatile 修飾的變量禁止 JVM 重排序。

如下代碼所示:

class VolatileOrder {

    int a, b, c;
    volatile int d;

    void write() {
        a = 1;
        b = 2;
        c = 3;
        d = 4;
    }

    void read() {
        int D = d;
        int A = a;
        int B = b;
        int C = c;
    }
}

在 write() 方法中:

a = 1;
b = 2;
c = 3;

JVM 可能會重排序這三個(gè)指令,但是這三個(gè)指令一定是排在 d = 4; 這個(gè)指令之前。

同樣的,在 read() 方法中:

int A = a;
int B = b;
int C = c;

JVM 可能會重排序這三個(gè)指令,但是這三個(gè)指令一定是排在 int D = d; 這個(gè)指令之后。

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

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容