认识 Java 的 volatile 关键字及指令重排

在多线程编程中，volatile 关键字是确保变量在多个线程之间可见的重要工具，它还能帮助防止指令重排。下面，我们将详细解释这些概念，并通过实际例子说明 volatile 的使用场景和局限性。

可见性

在多线程环境中，一个线程对共享变量的修改可能不会被其他线程立即看到。这是因为线程可能会将变量的值缓存，而不是直接从主内存中读取。例如：

Copy
class SharedObject {
private boolean flag = false;

public void setFlag() {
this.flag = true;
}

public boolean getFlag() {
return this.flag;
}
}

假设线程 A 调用 setFlag() 方法将 flag 设置为 true，而线程 B 调用 getFlag() 方法检查 flag 的值。在没有使用 volatile 的情况下，线程 B 可能看不到线程 A 对 flag 的修改，因为 flag 的更新可能只存在于线程 A 的缓存中，而没有同步到主内存。

volatile 如何解决可见性问题

通过将变量声明为 volatile，可以确保对这个变量的所有修改对所有线程都是可见的：

Copy
class SharedObject {
private volatile boolean flag = false;

public void setFlag() {
this.flag = true;
}

public boolean getFlag() {
return this.flag;
}
}

在这个例子中，flag 被声明为 volatile，这意味着每次对 flag 的写操作都会立刻更新到主内存，任何线程读取 flag 的值时都会直接从主内存中获取最新的值，从而确保了变量的可见性。

volatile 的局限性

虽然 volatile 能保证可见性，但它不能保证操作的原子性。原子性意味着操作要么全部成功，要么不成功。例如，以下代码中的 count++ 操作并不是原子的：

Copy
class Counter {
private volatile int count = 0;

public void increment() {
count++;
}
}

count++ 实际上包含三个步骤：

读取 count 的值。

增加值。

写回 count。

如果两个线程同时执行 increment() 方法，它们可能会读取到相同的 count 值，然后分别增加这个值，最终导致 count 的值少于实际增加的次数。

如何保证原子性

要保证操作的原子性，可以使用 synchronized 或 AtomicInteger 类：

使用 synchronized：

Copy
class Counter {
private int count = 0;

public synchronized void increment() {
count++;
}
}

使用 synchronized 可以确保同一时刻只有一个线程能够执行 increment() 方法，从而保证 count++ 操作的原子性。

使用 AtomicInteger：

Copy
import java.util.concurrent.atomic.AtomicInteger;

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

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

AtomicInteger 提供了原子操作的方法，如 incrementAndGet()，可以安全地进行并发操作。

指令重排

指令重排是编译器和 CPU 为了优化性能而对代码指令执行顺序进行的调整。这可能导致在多线程环境中出现意外行为。例如：

Copy
class Example {
private int x = 0;
private boolean flag = false;

public void method1() {
x = 1;
flag = true;
}

public void method2() {
if (flag) {
System.out.println(x);
}
}
}

在没有使用 volatile 的情况下，编译器或 CPU 可能将 flag = true 和 x = 1 的执行顺序调整，从而可能导致 method2 中的 flag 已变为 true 但 x 还未更新。

volatile 如何处理指令重排

volatile 关键字可以防止对 volatile 变量的指令重排。使用关键字后，写操作不会被重排到读操作之前，读操作不会被重排到写操作之后，从而避免了指令重排带来的问题：

Copy
class Example {
private volatile int x = 0;
private volatile boolean flag = false;

public void method1() {
x = 1;
flag = true;
}

public void method2() {
if (flag) {
System.out.println(x);
}
}
}

在这个例子中，flag 和 x 都被声明为 volatile，这样可以确保 method1 中的 flag = true 不会被重排到 x = 1 之前，从而在 method2 中可以正确地读取到 x 的最新值。

一句话

volatile 是 Java 中一个重要的工具，确保变量在多线程中对所有线程都是可见的，并防止指令重排。然而，它不能保证操作的原子性。在需要保证原子性的场景中，考虑使用 synchronized 或 AtomicInteger。

此文由 Mix Space 同步更新至 xLog
原始链接为 https://me.liuyaowen.club/posts/default/20240821and2