深入理解volatile

一、volatile概念介紹

在Java編程語(yǔ)言中,volatile是一個(gè)關(guān)鍵字,它用于修飾變量,以確保對(duì)該變量的讀寫操作都直接作用于主內(nèi)存,而不是線程的工作內(nèi)存。這使得volatile變量在多線程環(huán)境中具有可見(jiàn)性和有序性。

二、volatile的兩大特性

  1. 保證可見(jiàn)性:當(dāng)一個(gè)線程修改了一個(gè)被 volatile 修飾的變量后,新的值會(huì)立即反映到主內(nèi)存中,并且任何線程讀取這個(gè)變量都會(huì)從主內(nèi)存中讀取。因此,任何其他線程對(duì)該變量的讀取都會(huì)看到這個(gè)最新的更新。這有助于解決一些基本的并發(fā)問(wèn)題,使得變量的修改能夠被其他線程及時(shí)感知。

  2. 保證有序性:volatile 變量的讀寫具有一定的內(nèi)存屏障效果。它禁止了指令重排序(至少在某些方面),使得在讀寫 volatile 變量時(shí)不會(huì)發(fā)生重排序問(wèn)題,從而保證了執(zhí)行順序的一致性。這意味著在寫一個(gè) volatile 變量前的指令不會(huì)被重排序到寫操作之后,而在讀一個(gè) volatile 變量后的指令不會(huì)被重排序到讀操作之前。

注意:volatile 是一種簡(jiǎn)單而有效的輕量級(jí)同步機(jī)制,盡管volatile保證了可見(jiàn)性和有序性,但它并不保證復(fù)合操作的原子性。例如,對(duì)于i++這樣的操作,雖然i是volatile類型的,但是i++包含了讀取、增加和寫回三個(gè)步驟,這些步驟并不是原子的。因此在并發(fā)環(huán)境下仍然可能會(huì)出現(xiàn)競(jìng)態(tài)條件,導(dǎo)致數(shù)據(jù)不一致。所以還需要考慮使用更強(qiáng)大的同步手段如 synchronized 或者 Lock。

三、內(nèi)存屏障

內(nèi)存屏障(Memory Barrier),也稱為內(nèi)存柵欄,是一種同步機(jī)制,用于控制特定操作的執(zhí)行順序,確保在多處理器或多線程環(huán)境中內(nèi)存操作的一致性和有序性。內(nèi)存屏障的主要作用是防止編譯器和處理器對(duì)指令序列進(jìn)行重排序,從而保證內(nèi)存操作的順序性和可見(jiàn)性。

在 Java 中,我們不需要直接編寫內(nèi)存屏障的代碼,我們只需要直接使用 volatile 去修飾變量便會(huì)自動(dòng)帶來(lái)內(nèi)存屏障的效果。但是了解內(nèi)存屏障的概念有助于更好地理解 volatile 和其他同步機(jī)制的工作原理。

在多線程編程中,內(nèi)存屏障通常用于以下幾種情況:

  1. 編譯器重排序:編譯器在不改變程序語(yǔ)義的前提下,可能會(huì)對(duì)指令進(jìn)行重排序以優(yōu)化性能。內(nèi)存屏障可以防止這種重排序,確保指令按照程序員的意圖執(zhí)行。
  2. 處理器重排序:現(xiàn)代處理器為了提高執(zhí)行效率,可能會(huì)對(duì)指令進(jìn)行亂序執(zhí)行。內(nèi)存屏障可以確保在屏障前后的指令按照一定的順序執(zhí)行。
  3. 緩存一致性:在多核處理器系統(tǒng)中,每個(gè)核心可能有自己的緩存。內(nèi)存屏障可以確保一個(gè)核心對(duì)內(nèi)存的修改對(duì)其他核心可見(jiàn),從而維護(hù)緩存的一致性。

內(nèi)存屏障簡(jiǎn)分通常分為以下4種類型:

  1. Load Barrier(讀屏障):確保所有在讀屏障之前的讀操作完成后,才能執(zhí)行讀屏障之后的讀操作。
  2. Store Barrier(寫屏障):確保所有在寫屏障之前的寫操作完成后,才能執(zhí)行寫屏障之后的寫操作。
  3. Load-Store Barrier(讀-寫屏障):這種屏障結(jié)合了讀屏障和寫屏障的特性,確保在讀-寫屏障之前的讀操作和寫操作都完成后,才能執(zhí)行屏障之后的讀寫操作。
  4. Store-Load Barrier(寫-讀屏障):寫-讀屏障是最強(qiáng)大的內(nèi)存屏障,它確保在屏障之前的寫操作對(duì)所有后續(xù)的讀操作可見(jiàn)。這種屏障可以防止編譯器和處理器對(duì)讀寫操作進(jìn)行重排序,從而確保內(nèi)存操作的順序性和一致性。

四、案例代碼

1、volatile體現(xiàn)可見(jiàn)性案例:

package com.fivefox.thread1;
import java.util.concurrent.TimeUnit;
public class VolatileTest {

    private static volatile boolean flag = true;
    public static void main(String[] args) throws InterruptedException {
        new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + ",線程執(zhí)行中....");
            while (flag) {}
            System.out.println("flag被修改為false,threadA線程退出。");
        }, "threadA").start();

        TimeUnit.SECONDS.sleep(2);
        flag = false;
        System.out.println("已將flag設(shè)置false主線程代碼執(zhí)行完畢!");
    }
}
  • 不加volatile關(guān)鍵字:
    這個(gè)時(shí)候flag變量是不滿足可見(jiàn)性的,將會(huì)導(dǎo)致threadA一直在線程的工作內(nèi)存中讀取flag的值。從而導(dǎo)致程序一直處于while循環(huán)的狀態(tài)。
  • 加了volatile關(guān)鍵字后:
    這個(gè)時(shí)候flag變量滿足可見(jiàn)性了;當(dāng)main線程修改flag變量后,新的值會(huì)立即寫入到主內(nèi)存中。并且任何線程對(duì)于flag變量的讀取都會(huì)從主內(nèi)存中讀取。這個(gè)時(shí)候while檢測(cè)到flag值變?yōu)閒alse,將會(huì)跳出while,結(jié)束程序的執(zhí)行。

2、volatile不滿足原子性案例:

public class VolatileCounter {
    private volatile int count = 0;

    public void increment() {
        count++;
    }
    public int getCount() {
        return count;
    }
}
package com.fivefox.thread1;

public class VolatileCounterTest {
    public static void main(String[] args) throws InterruptedException {
        final VolatileCounter counter = new VolatileCounter();
        final int threadCount = 100;

        // 創(chuàng)建線程并啟動(dòng)
        Thread[] threads = new Thread[threadCount];
        for (int i = 0; i < threadCount; i++) {
            threads[i] = new Thread(() -> {
                for (int j = 0; j < 100; j++) {
                    counter.increment();
                }
            });
            threads[i].start();
        }
        // 等待所有線程結(jié)束
        for (Thread t : threads) {
            t.join();
        }
        // 輸出最終的計(jì)數(shù)值
        System.out.println("Final count: " + counter.getCount());
    }
}

在這個(gè)例子中,我們創(chuàng)建了 1000 個(gè)線程,每個(gè)線程都會(huì)對(duì) count 進(jìn)行 1000 次遞增操作。理論上,最終的計(jì)數(shù)值應(yīng)該是 100 * 100 = 10000。然而,由于 count++ 是一個(gè)復(fù)合操作,它包括讀取當(dāng)前值、計(jì)算新值和寫回新值三個(gè)步驟,因此在多線程環(huán)境下可能會(huì)出現(xiàn)競(jìng)態(tài)條件,導(dǎo)致最終計(jì)數(shù)值小于預(yù)期。

這里的關(guān)鍵在于 count++ 并不是一個(gè)原子操作。即使 count 被聲明為 volatile,這也只能保證每次讀取 count 時(shí)都會(huì)得到最新的值,但無(wú)法保證整個(gè) count++ 操作的原子性。因此,當(dāng)多個(gè)線程幾乎同時(shí)執(zhí)行 count++ 時(shí),可能會(huì)導(dǎo)致某些線程讀取相同的 count 值,然后各自進(jìn)行遞增并寫回,從而導(dǎo)致實(shí)際的計(jì)數(shù)值比預(yù)期的小。

確保 count++ 的原子性的方法:

  1. 可以使用 java.util.concurrent.atomic.AtomicInteger 類來(lái)替代普通的 int 變量。AtomicInteger 內(nèi)部使用了 CAS (Compare and Swap) 操作來(lái)確保復(fù)合操作的原子性。
public class VolatileCounter {
// private volatile int count = 0;
private AtomicInteger count = new AtomicInteger(0);
    public void increment() {
        count++;
    }
    public int getCount() {
        return count;
    }
}

 public void increment() {
        count.incrementAndGet();
 }
  1. 通過(guò)引入鎖機(jī)制(ReentrantLock),可以確保 increment() 方法中的 count++ 操作在一個(gè)互斥的上下文中執(zhí)行,從而保證了該操作的原子性。這種方法非常適合用于復(fù)合操作,因?yàn)殒i可以確保在同一時(shí)刻只有一個(gè)線程能夠執(zhí)行被鎖定的代碼段。
package com.fivefox.thread1;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class VolatileCounter {
    private Lock lock = new ReentrantLock();
    private volatile int count = 0;

    public void increment() {
        lock.lock();
        try {
            count++;
        } finally {
            lock.unlock();
        }
    }
    public int getCount() {
        return count;
    }
}

3、DCL 單例模式

雙重檢查鎖定(Double-Checked Locking, DCL)在單例模式中的應(yīng)用。雙重檢查鎖定是一種常用的線程安全技術(shù),用于確保在多線程環(huán)境下單例對(duì)象只能被創(chuàng)建一次,并且每次請(qǐng)求都能得到同一個(gè)實(shí)例。

在實(shí)際開(kāi)發(fā)中,雙重檢查鎖定通常用于高并發(fā)場(chǎng)景下的單例模式實(shí)現(xiàn)。它既能保證線程安全,又能提高性能。

public class Singleton {
    // 使用 volatile 關(guān)鍵字確??梢?jiàn)性和有序性
    private static volatile Singleton instance;

    // 私有構(gòu)造器,防止外部實(shí)例化
    private Singleton() {}

    // 提供一個(gè)靜態(tài)公共方法來(lái)獲取單例對(duì)象
    public static Singleton getInstance() {
        // 第一次檢查:如果實(shí)例不存在,則進(jìn)入同步代碼塊
        if (instance == null) {
            synchronized (Singleton.class) {
                // 第二次檢查:確保在多線程環(huán)境下只有一個(gè)實(shí)例被創(chuàng)建
                if (instance == null) {
                    // 實(shí)例化單例對(duì)象
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}
  1. 為什么要使用 DCL?
    直接使用同步方法或同步代碼塊會(huì)導(dǎo)致性能問(wèn)題,因?yàn)槊看卧L問(wèn)都需要加鎖,即使已經(jīng)創(chuàng)建了實(shí)例也是如此。雙重檢查鎖定通過(guò)只在必要時(shí)才進(jìn)行同步來(lái)提高效率。
  2. 為什么需要兩次檢查?
    第一次檢查:如果 instance 已經(jīng)被創(chuàng)建,那么不需要再進(jìn)入同步代碼塊,這樣可以避免不必要的同步開(kāi)銷。
    第二次檢查:確保即使多個(gè)線程同時(shí)進(jìn)入第一次檢查,也只會(huì)有一個(gè)線程成功創(chuàng)建實(shí)例。
  3. 為什么需要 volatile?
    可見(jiàn)性:volatile 確保當(dāng)一個(gè)線程修改了 instance 變量后,其他線程能夠立即看到這個(gè)修改。
    有序性:volatile 防止編譯器和處理器重排序,確保 instance 的寫操作不會(huì)與構(gòu)造函數(shù)中的其他操作重排。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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

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