共计 1729 个字符,预计需要花费 5 分钟才能阅读完成。
volatile介绍
首先要知道的是并发编程的三个特性:可见性、有序性和原子性,其中 volatile 只能保证其中的可见性和有序性,并不能保证原子性。
- 可见性
可见性指一个线程对共享变量的修改能够被其他线程立即看到的特性。
多线程条件下,线程0和线程1同时读取了 flag 变量,但线程1将 flag 变量修改后,不会立即刷回主存(常说的内存),而是保存在自己的缓存中(CPU 缓存)。那么此时线程0需要对 flag 变量进行修改时,还不知道 flag 变量已经改变了,所以此时这个变量是线程之间不可见的。
但是如果使用了 volatile 关键字修饰的变量,线程修改后会立即刷入主内存。其他线程在访问这个 volatile 变量时,都会从主内存中重新读取它的最新值,从而保证数据的一致性。
- 有序性
有序性指程序执行的顺序必须符合预期,不能出现乱序的情况。
例如有如下代码:
int a = 100;
int b = 10;
int a = a + 10;
但经过编译器编译后,为了提高执行效率,可能优化成顺序如下:
int a = 100;
int a = a + 10;
int b = 10;
这种情况下程序就不是严格按照代码编写的顺序执行的,在某些情况下可能会出现问题。
例如:
public class Example {
private boolean ready = false;
private int number;
public void writer() {
number = 42; // 步骤1
ready = true; // 步骤2
}
public void reader() {
if (ready) { // 步骤3
int localNumber = number; // 步骤4
System.out.println("Number is: " + localNumber);
}
}
}
如果在多线程条件下,步骤1和步骤2的指令重排了,那么 reader 方法,有可能会读取到没有初始化的 number 值。
- 原子性
原子性指一个操作是不可分割的、完整的,要么全部执行成功,要么全部不执行,不存在执行一半的情况。在多线程环境下,如果一个操作不是原子性的,那么可能会发生竞态条件(race condition)等问题,导致程序出现不可预期的错误。为了保证原子性,可以使用 synchronize d关键字或者使用 Atomic 类中提供的原子操作。
如下案例:当线程0修改变量后立即刷回主内存,但由于线程1已经读取并将变量用于运算了,只差保存到工作内存中,此时及时线程1中的变量失效,也不会影响线程1步骤的执行。所以即使线程0将变量修改为1,线程1同样还是修改为1,只是覆盖了而已。
volatile和synchronized比较
特性 | volatile |
synchronized |
---|---|---|
可见性 | 保证 | 保证 |
原子性 | 不保证 | 保证 |
指令重排序 | 禁止 | 禁止 |
性能开销 | 较低 | 较高(涉及锁机制) |
使用场景 | 轻量级状态标识、多线程间变量共享 | 需要原子性操作的临界区 |
需要注意的是 volatile 和 synchronize 禁止指令重排序的控制的颗粒度不同,synchronize 禁止的是它包含的代码块中的指令重排序,它并不能在指令层面禁止重排序。
这也是为什么双重检查锁单例模式需要 volatile 关键字的原因。由于对象的初始化分为三个阶段,开辟内存空间、内存空间初始化、将对象的地址赋值给引用。如果不使用 volatile,有可能开辟内存空间后直接将地址赋值给引用,那么其他线程判断对象不为空,直接使用则会出现问题。
public class Singleton {
// 使用volatile关键字确保多线程环境下的可见性和禁止指令重排
private static volatile Singleton instance;
// 私有构造函数,防止外部通过new创建实例
private Singleton() {
}
// 提供一个全局访问点
public static Singleton getInstance() {
// 第一次检查,避免不必要的锁定
if (instance == null) {
// 同步块,确保只有一个线程能执行以下代码
synchronized (Singleton.class) {
// 第二次检查,避免重复创建实例
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}