原子性的意義
原子性特別是在併發編程領域,是一個極其重要的概念,原子性指的是一個操作或一組操作要麼全部執行成功,要麼全部不執行,不會出現部分執行的情況。這意味着原子性操作是不可分割的,它們在執行過程中不會被其他操作中斷或干擾。
原子性的意義在於它保證了數據的一致性和程序的正確性。在多線程或多進程的環境中,當多個操作同時訪問和修改共享數據時,如果沒有原子性保證,可能會導致數據不一致或不確定的結果。例如,如果一個線程在讀取某個數據時,另一個線程同時修改了這個數據,那麼第一個線程讀取到的數據可能是不正確的。通過確保操作的原子性,可以避免這種情況,從而維護數據的完整性和程序的正確執行。
瞭解了上面的原子性的重要概念後,接下來一起聊一聊 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。但對於複合操作,可以使用上述其他方法來實現,通過這些方法,可以確保在多線程環境中對共享資源的正確同步和可見性。