Volatile 关键字
Volatile 保证变量可见性
Java程序中,被volatile
关键字修饰的变量的特点:
- 可见性
- 有序性
- 不保证原子性
在Java中,volitail
关键字修饰的变量将会指示JVM,这个变量共享且不稳定,每次使用它都需要从主存中读取。
volatile
关键字并不是Java特有的,在C语言中也存在,该关键字最原始的意义就是禁止CPU缓存。
📢注意:volatile
关键字能够保证数据的可见性,但是不保证数据的原子性。(synchronized
二者都能保证)
volatile的内存语义
- 当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值立即刷新会主内存中
- 当读一个volatile变量时,JMM会把该线程对应的本地内存设置为无效,直接从主内存中读取共享变量
=> volatile
内存语义:
- 写内存语义:直接刷新到主存中
- 读内存语义:直接从主内存中读取
禁止指令重排序
Java中,volatile
关键字除了可以保证变量的可见性,还有一个重要的作用就是保证指令的有序性,也就是防止JVM的指令重排序。使用volatile修饰变量时,在对这个变量进行读写时,会通过插入特定的内存屏障来禁止指令重排序。
【指令重排序经典例子:单例模式】双检锁实现对象单例(线程安全)
public class Singleton { |
上述代码中,使用volatile给变量INSTANCE是有必要的,因为实例化的代码INSTANCE = new Singleton();
在字节码层面是分为三步进行的:
- 为INSTANCE分配内存空间
- 初始化INSTANCE
- 将INSTANCE执行分配的内存地址
上述步骤的顺序是不可以进行重排序的。指令重排序在单线程环境下不会出现问题,但是在多线程环境下会导致一个线程获取到还没有初始化的实例对象的问题。
不使用
volatile
的单例模式就是使用饿汉式,声明的时候就进行初始化。
内存屏障
概念:内存屏障(内存栅栏、内存栅障、屏蔽指令),是CPU或者编译器在对内存随机访问的操作中的一个同步点,使得同步点之前的所有的读写操作都执行后才可以开始执行同步点之后的操作,避免指令重排序。
内存屏障就是一种JVM指令,JMM的重排规则会要求Java编译器在生成JVM指令时插入特定的内存屏障指令,通过这些内存屏障,volatile实现了JMM中的可见性和有序性。
在Java中,Unsafe
类提供了三个内存屏障相关的本地方法,屏蔽了操作系统底层的差异:
/** |
volatile不保证原子性
代码证明:
public class Demo { |
正常情况下,上述代码的结果应该是2500,但实际上每次的输出结果都会小于2500。
原因是num++
并不是一个原子操作,它包括三个步骤:
- 读取num的值
- num + 1操作
- 将num的值写会内存
volatile是无法保证这三个操作具有原子性的。
解决方案:使用
synchronized
、Lock
或者原子类AtomicInteger
都可以实现
volatile应用场景
1)单一赋值的场景
volatile int a = 10; |
📢注意: a++或者++a这类操作不属于单一赋值操作,是复合运算操作
2)状态标志,用于判断业务是否结束
public class UseVolatileDemo{ |
3)开销较低的读写锁策略
public class UseVolatileDemo{ |
参考文章
- https://blog.csdn.net/TZ845195485/article/details/117601980
- https://javaguide.cn/java/concurrent/java-concurrent-questions-02.html