Volatile不保證原子性及解決方案

威哥爱编程發表於2024-07-19

原子性的意義

原子性特別是在併發程式設計領域,是一個極其重要的概念,原子性指的是一個操作或一組操作要麼全部執行成功,要麼全部不執行,不會出現部分執行的情況。這意味著原子性操作是不可分割的,它們在執行過程中不會被其他操作中斷或干擾。

原子性的意義在於它保證了資料的一致性和程式的正確性。在多執行緒或多程序的環境中,當多個操作同時訪問和修改共享資料時,如果沒有原子性保證,可能會導致資料不一致或不確定的結果。例如,如果一個執行緒在讀取某個資料時,另一個執行緒同時修改了這個資料,那麼第一個執行緒讀取到的資料可能是不正確的。透過確保操作的原子性,可以避免這種情況,從而維護資料的完整性和程式的正確執行。

瞭解了上面的原子性的重要概念後,接下來一起聊一聊 volatile 關鍵字。

volatile 關鍵字在 Java 中用於確保變數的更新對所有執行緒都是可見的,但它並不保證複合操作的原子性。這意味著當多個執行緒同時訪問一個 volatile 變數時,可能會遇到讀取不一致的問題,儘管它們不會看到部分更新的值。

Volatile 的限制

  • 不保證原子性:volatile 變數的單個讀寫操作是原子的,但複合操作(如自增或同步塊)不是原子的。
  • 不保證順序性:volatile 變數的讀寫操作不會與其他操作(如非 volatile 變數的讀寫)發生重排序。

一個例子

用一個示例來解釋會更清楚點,假如我們有一段程式碼是這樣的:

class Counter {
    private volatile int count = 0;

    void increment() {
        count++;
    }

    int getCount() {
        return count;
    }
}

儘管 count 是 volatile 變數,但 increment 方法中的複合操作 count++(讀取-增加-寫入)不是原子的。因此,在多執行緒環境中,多個執行緒可能會同時讀取相同的初始值,然後增加它,導致最終值低於預期。

volatile 不保證原子性的程式碼驗證

以下是一個簡單的 Java 程式,演示了 volatile 變數在多執行緒環境中不保證複合操作原子性的問題:


public class VolatileTest {
    private static volatile int counter = 0;

    public static void main(String[] args) throws InterruptedException {
        int numberOfThreads = 10000;
        Thread[] threads = new Thread[numberOfThreads];

        for (int i = 0; i < numberOfThreads; i++) {
            threads[i] = new Thread(() -> {
                for (int j = 0; j < 100; j++) {
                    counter++;
                }
            });
            threads[i].start();
        }

        for (int i = 0; i < numberOfThreads; i++) {
            threads[i].join();
        }

        System.out.println("Expected count: " + (numberOfThreads * 100));
        System.out.println("Actual count: " + counter);
    }
}

在這個例子中:

  • counter 是一個 volatile 變數。
  • 每個執行緒都會對 counter 執行 100 次自增操作。
  • 理論上,如果 counter++ 是原子的,最終的 counter 值應該是 10000 * 100。

然而,由於 counter++ 包含三個操作:讀取 counter 的值、增加 1、寫回 counter 的值,這些操作不是原子的。因此,在多執行緒環境中,最終的 counter 值通常會小於預期值,這證明了 volatile 變數不保證複合操作的原子性。

解決方案

1. 使用 synchronized 方法或塊:

  • 將訪問 volatile 變數的方法或程式碼塊宣告為 synchronized,確保原子性和可見性。
class Counter {
    private volatile int count = 0;

    synchronized void increment() {
        count++;
    }

    synchronized int getCount() {
        return count;
    }
}

2. 使用 AtomicInteger 類:

java.util.concurrent.atomic 包中的 AtomicInteger 提供了原子操作,可以替代 volatile 變數。


import java.util.concurrent.atomic.AtomicInteger;

class Counter {
    private AtomicInteger count = new AtomicInteger(0);

    void increment() {
        count.incrementAndGet();
    }

    int getCount() {
        return count.get();
    }
}

3. 使用鎖(如 ReentrantLock):

使用顯式鎖(如 ReentrantLock)來同步訪問 volatile 變數的程式碼塊。


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

class Counter {
    private volatile int count = 0;
    private final Lock lock = new ReentrantLock();

    void increment() {
        lock.lock();
        try {
            count++;
        } finally {
            lock.unlock();
        }
    }

    int getCount() {
        lock.lock();
        try {
            return count;
        } finally {
            lock.unlock();
        }
    }
}

使用volatile變數的正確使用場景

如果操作是簡單的讀寫,並且你只需要保證可見性,可以使用 volatile。但對於複合操作,可以使用上述其他方法來實現,透過這些方法,可以確保在多執行緒環境中對共享資源的正確同步和可見性。

相關文章