Volatile
1. 防止指令重排序 (Prevent Reordering)
-
问题: 指令重排序可能导致多线程环境下程序出现意想不到的结果,尤其是在对象实例化等场景。
-
示例:双重检查锁 (DCL) 单例模式
public class Singleton { public static volatile Singleton singleton; // 使用 volatile 关键字 private Singleton() {}; public static Singleton getInstance() { if (singleton == null) { synchronized (Singleton.class) { if (singleton == null) { singleton = new Singleton(); } } } return singleton; } }
- 原因:
singleton = new Singleton();
对象实例化通常分为三个步骤:- 分配内存空间。
- 初始化对象。
- 将内存地址赋值给引用
singleton
。
- 重排序风险: 指令重排序可能导致步骤 2 和 3 颠倒顺序,即先将未初始化的对象引用赋值给
singleton
,此时如果另一个线程访问getInstance()
,可能获得未完成初始化的对象,导致错误。 volatile
的作用:volatile
关键字禁止singleton
变量赋值时的指令重排序,确保对象初始化完成后,引用才被其他线程可见。
- 原因:
2. 实现可见性 (Visibility)
-
问题: 线程拥有自己的工作内存 (高速缓存),对共享变量的修改可能不会立即同步到主内存,导致其他线程读取到过期的值,产生可见性问题。
-
volatile
的作用:volatile
关键字强制线程每次使用变量时都从主内存中读取,每次修改后都立即写回主内存,保证所有线程看到的是变量的最新值。 -
示例:
``` public class TestVolatile { private static volatile boolean stop = false; // 使用 volatile 关键字
public static void main(String[] args) { new Thread("Thread A") { @Override public void run() { while (!stop) { // 线程 A 循环读取 stop 变量 } System.out.println(Thread.currentThread() + " stopped"); } }.start(); try { TimeUnit.SECONDS.sleep(1); System.out.println(Thread.currentThread() + " after 1 seconds"); } catch (InterruptedException e) { e.printStackTrace(); } stop = true; // 线程 Main 修改 stop 变量 }
} ```
- 结果: 加上
volatile
后,线程 Main 修改stop
的值后,线程 A 能立即看到修改,并停止循环。
- 结果: 加上
3. 保证原子性 (Atomicity): 单次读/写
-
局限性:
volatile
不能保证复合操作的原子性,例如i++
。 -
i++
非原子性示例:``` public class VolatileTest01 { volatile int i; // volatile 变量 i
public void addI(){ i++; // i++ 复合操作 } public static void main(String[] args) throws InterruptedException { final VolatileTest01 test01 = new VolatileTest01(); for (int n = 0; n < 1000; n++) { new Thread(new Runnable() { @Override public void run() { try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } test01.addI(); } }).start(); } Thread.sleep(10000); System.out.println(test01.i); // 结果通常小于 1000 }
} ```
- 原因:
i++
操作包含 读取-修改-写入 三个步骤,volatile
只能保证每次读写i
的操作是原子的,但无法保证这三个步骤作为一个整体是原子的。 - 解决方案: 使用
AtomicInteger
原子类或synchronized
关键字来保证i++
的原子性。 -
long
和double
类型的原子性: -
对于
long
和double
类型,普通读写操作在某些平台下可能不是原子的,会被分成高 32 位和低 32 位两次操作。 volatile
可以保证long
和double
类型的单次读写操作的原子性。- 现代 JVM 实现: 大多数商用 JVM 已经将 64 位数据的读写操作作为原子操作,因此通常不需要特意将
long
和double
声明为volatile
。
- 原因:
4. Volatile 的实现原理
volatile
的可见性和有序性是通过 内存屏障 (Memory Barrier) 实现的。
- 内存屏障: CPU 指令,用于限制编译器和 CPU 的指令重排序,并保证缓存一致性。
lock
前缀指令:volatile
变量写操作会被编译成汇编代码时,会多出一个lock
前缀指令。lock
前缀指令会触发以下两个关键动作:- 将当前处理器缓存行的数据写回系统内存 (主内存)。
- 使其他 CPU 缓存了该内存地址的数据失效。
- 缓存一致性协议 (MESI): 用于保证多核处理器缓存之间数据一致性的协议。当一个处理器修改了共享变量,会通知其他处理器,使其缓存失效,需要重新从主内存读取数据。
5. Volatile 的 Happens-Before 关系
-
Volatile 变量规则: 对一个
volatile
域的写,Happens-Before 于任意后续对这个volatile
域的读。``` class VolatileExample { int a = 0; volatile boolean flag = false;
public void writer() { a = 1; // 1. 普通写 flag = true; // 2. volatile 写 } public void reader() { if (flag) { // 3. volatile 读 int i = a; // 4. 普通读 // ... } }
} ```
- Happens-Before 关系:
- 程序顺序规则: 1 happens-before 2, 3 happens-before 4
- Volatile 变量规则: 2 happens-before 3
- 传递性规则: 1 happens-before 4
- 保证: 线程 A 修改
volatile
变量flag
为true
后,线程 B 读取flag
时,能够看到最新的flag
值,并且也能看到线程 A 在flag = true
之前对a = 1
的修改。
- Happens-Before 关系:
6. Volatile 的应用场景
使用 volatile
的条件:
- 写操作不依赖当前值: 例如,状态标志
boolean flag = true;
,赋值操作x = y;
。 - 变量不包含在与其他变量的不变式中:
volatile
只能保证单个变量的可见性和有序性,无法保证多个变量之间的复合操作的原子性。 - 状态独立于程序其他部分: 变量的状态变化不影响程序其他逻辑的正确性。
常见使用模式:
-
状态标志 (Status Flag): 指示一次性事件是否发生,例如程序是否需要停止。
volatile boolean shutdownRequested; public void shutdown() { shutdownRequested = true; } public void doWork() { while (!shutdownRequested) { // ... } }
-
一次性安全发布 (One-time Safe Publication): 安全发布只写一次,多线程读取的对象引用。
public class BackgroundFloobleLoader { public volatile Flooble theFlooble; // volatile 对象引用 public void initInBackground() { theFlooble = new Flooble(); // 只写一次 } } public class SomeOtherClass { public void doWork() { if (floobleLoader.theFlooble != null) // 多线程读取 doSomething(floobleLoader.theFlooble); } }
-
独立观察 (Independent Observation): 定期发布观察结果供程序内部使用,例如温度传感器数据。
public class UserManager { public volatile String lastUser; // volatile 变量记录最新用户名 public boolean authenticate(String user, String password) { if (passwordIsValid(user, password)) { lastUser = user; // 更新 volatile 变量 return true; } return false; } }
-
Volatile Bean 模式: JavaBean 的所有数据成员都声明为
volatile
,getter/setter 方法简单,对象引用成员指向不可变对象。@ThreadSafe public class Person { private volatile String firstName; private volatile String lastName; private volatile int age; // ... getter and setter methods ... }
-
低开销的读-写锁策略 (Cheap Read-Write Lock Trick): 读多写少场景,结合
synchronized
(写操作原子性) 和volatile
(读操作可见性) 优化性能。``` @ThreadSafe public class CheesyCounter { @GuardedBy("this") private volatile int value; // volatile 变量
public int getValue() { return value; } // 读操作 - volatile 读 public synchronized int increment() { // 写操作 - synchronized 保证原子性 return value++; }
} ```
-
双重检查 (Double-Checked Locking): 单例模式的一种实现方式。
class Singleton {
private volatile static Singleton instance;
private Singleton() {
}
public static Singleton getInstance() {
if (instance == null) {
syschronized(Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
1. volatile
关键字的作用是什么?
volatile
关键字是 Java 提供的一种轻量级的同步机制,主要有两个作用:
- 可见性:
- 确保被
volatile
修饰的变量,当一个线程修改了它的值,这个新值对于其他线程来说是立即可见的。 - 也就是说,当一个线程写入
volatile
变量时,JVM 会强制将修改后的值立即写入主内存;当另一个线程读取该变量时,JVM 会强制其从主内存中读取最新值。
- 确保被
- 有序性(禁止指令重排序):
- 防止编译器和处理器对指令进行重排序优化。
volatile
关键字会插入内存屏障,保证特定的操作顺序,避免在多线程环境下出现意料之外的结果。
2. volatile
能保证原子性吗?
- 不能。
volatile
只能保证可见性和有序性,但不能保证原子性。- 原子性是指一个操作是不可中断的,要么全部执行成功,要么全部执行失败。
- 例如,
i++
这样的操作,即使i
被volatile
修饰,仍然不是原子操作,因为它包含三个步骤:读取i
的值、将i
的值加 1、将结果写回i
。在多线程环境下,这三个步骤可能会被其他线程中断,导致结果错误。
3. 之前 32 位机器上共享的 long
和 double
变量的为什么要用 volatile
?现在 64 位机器上是否也要设置呢?
- 在 32 位 JVM 中,
long
和double
类型的变量的写操作可能会被拆分成两个 32 位的操作,这就导致在多线程环境下,可能会出现一个线程只读取到部分修改的情况。 volatile
可以保证这些变量的写操作是原子性的。- 在 64 位 JVM 中,
long
和double
类型的写操作通常是原子性的,但为了保证跨平台的兼容性和代码的清晰性,仍然建议在多线程环境下使用volatile
修饰这些变量。
4. i++
为什么不能保证原子性?
i++
操作不是原子性的,因为它包含以下三个步骤:- 读取
i
的值。 - 将
i
的值加 1。 - 将结果写回
i
。
- 读取
- 在多线程环境下,这三个步骤可能会被其他线程中断,导致以下情况:
- 线程 A 读取
i
的值。 - 线程 B 读取
i
的值。 - 线程 A 将
i
的值加 1 并写回。 - 线程 B 将
i
的值加 1 并写回。 - 最终,
i
的值只被加了 1,而不是 2。
- 线程 A 读取
5. volatile
是如何实现可见性的?内存屏障。
volatile
通过内存屏障来实现可见性。- 当一个线程写入
volatile
变量时,JVM 会在该写操作后插入一个写内存屏障,强制将修改后的值立即写入主内存。 - 当另一个线程读取
volatile
变量时,JVM 会在该读操作前插入一个读内存屏障,强制其从主内存中读取最新值。
6. volatile
是如何实现有序性的? happens-before 等
volatile
通过内存屏障和 happens-before 规则来实现有序性。- 内存屏障会禁止指令重排序,保证特定的操作顺序。
- happens-before 规则定义了内存操作的可见性顺序,保证在多线程环境下,一个操作的结果对另一个操作是可见的。
volatile
关键字的 happens-before 规则是:对一个volatile
变量的写操作 happens-before 后续对这个volatile
变量的读操作。
7. 说下 volatile
的应用场景?
- 状态标志:
- 用于标记某个状态是否发生变化,例如,控制线程是否退出的标志。
- 单例模式(DCL):
- 在双重检查锁定(DCL)的单例模式中,使用
volatile
修饰单例对象,防止指令重排序导致的问题。
- 在双重检查锁定(DCL)的单例模式中,使用
- 发布-订阅模式:
- 用于在发布者和订阅者之间传递数据,保证数据的可见性。
- 计数器:
- 在不要求原子性的情况下,可以使用volatile修饰计数器。