volatile其主要功能是保持线程可见性和防止指令重排序
保持线程可见性
其底层使用MESI协议,当值发生改变时,将其写入主内存中,并使其他CPU的缓存失效
具体案例:
public static void main(String[] args) {
//线程进入sleep,让第三个线程缓存volatile的值
new Thread(() -> {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
//当前缓存修改为true
flag = true;
System.out.println(flag);
}).start();
new Thread(() -> {
try {
Thread.sleep(200);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
//由于t2线程晚于t1线程执行,所以读到的是t1的缓存
System.out.println(flag);
}).start();
new Thread(() -> {
int i = 0;
while (!flag) {
//由于最早执行,flag缓存false且缓存修改未被通知,导致一直循环
i++;
}
}).start();
}
}
防止指令重排序
在多线程环境中,Java 虚拟机和处理器为了提高性能,可能会对代码指令进行重排序(指令重排),以提高执行效率。重排序不会影响单线程程序的执行结果,但在多线程环境中,如果对共享变量的访问顺序发生了变化,就可能导致数据不一致的问题。
其底层使用内存屏障来防止指令重排序
Java 对 volatile 变量的读写会在底层生成以下两种内存屏障:
读屏障(Load Barrier):在读取 volatile 变量之前,会插入一个读屏障,确保在读取 volatile 变量时,之前对该变量的所有写操作已经完成(保证可见性)。
写屏障(Store Barrier):在写入 volatile 变量之后,会插入一个写屏障,确保在对 volatile 变量进行写操作后,该操作不会被重排序到其他写操作之后。
具体规则:
volatile 读操作前,会插入一个读屏障,保证该操作不会被重排序到之前的操作之后,确保之前的操作已经对所有线程可见。
volatile 写操作后,会插入一个写屏障,确保该操作不会被重排序到之后的操作之前,保证该操作对于其他线程可见。
volatile是否为原子性的?
否!
原子性指的是一个操作不可被打断,要么完全执行,要么完全不执行
像 i++ 这样的复合操作实际上包括了以下几个步骤:
- 从内存中读取变量 i 的值。
- 在 i 的值上加 1。
- 将结果写回内存。
尽管 volatile 能保证其他线程可以看到最新的 i 值,但是由于 i++ 是一个复合操作,这三个步骤不是原子的。可能发生的情况是,多个线程同时读取到相同的 i 值,分别执行加 1 操作,并将相同的结果写回内存,导致加法结果丢失,产生线程安全问题。
怎么保证原子性?
- 使用Lock接口下的锁,如ReentranLock
- 使用关键字sychronized
- 使用原子类AtomicInteger等