volatile 关键字

volatile是Java中的关键字,它用于确保多线程环境下变量的可见性和有序性,但不能保证原子性。volatile可以说是Java虚拟机提供的最轻量级的同步机制,Java内存模型对volatile专门定义了一些特殊的访问规则。使用volatile修饰的共享变量(也称volatile 变量),具备三个特性:
(1) 保证操作的可见性。保证了不同线程对volatile变量操作的内存可见性;
(2) 无法保证操作的原子性。因为volatile关键字无法保证“原子性”,所以volatile变量的写操作无法保证原子性。
(3) 保证操作的有序性。volatile关键字通过禁止指令重排序,可以确保多个操作的有序性。

volatile关键字特性

保证操作的可见性

根据Java内存模型变量,变量在线程间传递均需要通过主内存来完成,例如,线程A修改一个变量的值,然后向主内存进行回写,另外一条线程B在线程A回写完成了之后再从主内存进行读取操作,新变量值才会对线程B可见。
对于 volatile 变量,在一个线程的工作内存被修改后,会立即同步回主内存,另一个线程每次在使用该 volatitle 变量时,都必须重新从主内存加载,进而保证可见性。
对可见性的误解,认为以下描述成立:“volatile变量对所有线程是立即可见的,对volatile变量所有的写操作都能立刻反应到其他线程之中,换句话说,volatile变量在各个线程中是一致的,所以基于volatile变量的运算在并发下是安全的”。这句话的论据部分并没有错,但是其论据并不能得出“基于volatile变量的运算在并发下是安全的”这个结论。volatile变量在各个线程的工作内存中不存在一致性问题(在各个线程的工作内存中,volatile变量也可以存在不一致的情况,但由于每次使用之前都要先刷新,执行引擎看不到不一致的情况,因此可以认为不存在一致性问题),但是Java里面的运算并非原子操作,导致volatile变量的运算在并发下一样是不安全的。
对保证操作的可见性,正确的描述是,当一个线程修改了一个volatile变量的值,其他线程会立即看到这个修改。这是因为volatile关键字会禁止CPU缓存和编译器优化,确保每次读取变量时都会从主内存中获取最新的值,从而确保了变量的可见性。

无法保证操作原子性

因为volatile关键字无法保证“原子性”,所以使用volatile关键字修饰的变量如果是原子性的操作,则具备原子性,否则必须使用syncronized或Lock对象等保证原子性。 对于不符合以下两条规则的运算场景,均需通过加锁来保证原子性:
(1) 运算结果并不依赖变量的当前值,或者能够确保只有单一的线程修改变量的值。
(2) 变量不需要与其他的状态变量共同参与不变约束。
使用volatile变量控制并发的示例代码如下:

volatile boolean shutdownRequested;

public void shutdown() {
    shutdownRequested = true;
}

public void doWork() {
    while(!shutdownRequested) {
        // do something
    }
}

保证操作的有序性

volatile关键字可以确保多个操作的有序性。即在一个线程中执行的操作,在另一个线程中也会按照相同的顺序执行。这是因为在Java内存模型中,一个线程中的操作会被排序,形成一个操作序列。但是,不同的线程可能看到操作序列的不同顺序。volatile关键字可以确保多个操作之间的顺序性,即一个线程中的操作会按照相同的顺序执行。上述能力也称为禁止指令重排序。指令重排序是指:代码书写的顺序与代码实际执行顺序不同,指令重排序是编译器或者处理器为了提高程序性能做出的优化。(编译成机器码后,重新调整下顺序,可能更符合CPU的特点,能最大限度的发挥CPU的性能)。
指令重排序不是指令任意重排或指令任意排序,CPU需要能正确处理指令依赖情况以保障程序能得出正确的执行结果。譬如指令1把地址A中的值加10,指令2把地址A中的值乘以2,指令3把地址B中的值减去3,这时指令1和指令2是有依赖的,其顺序不能重排——(A+10)2与A2+10显然不相等,但指令3可以重排到指令1、 2之前或者中间,只要保证CPU执行后面依赖到A、 B值的操作时能获取到正确的A和B值即可。
普通的变量仅仅会保证在该方法的执行过程中所有依赖赋值结果的地方都能获取到正确的结果,而不能保证变量赋值操作的顺序与程序代码中的执行顺序一致。因为在一个线程的方法执行过程中无法感知到这点,这也就是Java内存模型中描述的所谓的“线程内表现为串行的语义”(Within-Thread As-If-SerialSemantics)。
但是,指令重排序后干扰程序的并发执行,使其无法达到“代码书写顺序”的效果。指令重排序功能在JDK 1.5之后才真正实现。

volatile底层实现

加入volatile关键字的代码会多出一个lock前缀指令。lock前缀指令实际相当于一个内存屏障(Memory Barrier或Memory Fence),内存屏障提供了以下功能:
1.指令重排序时不能把后面的指令重排序到内存屏障之前的位置(禁止指令重排序);
2.使得本CPU的Cache写入内存,该写入动作也会引起别的CPU或者别的内核无效化其Cache,相当于让新写入的值对别的线程可见

volatile变量规则简介

假设T表示一个线程,V1和V2分别表示两个volatile类型变量,那么在进行read、load、use、assign、store和write操作时需要满足以下规则:
(1) 读可见性
只有当线程T对变量V1执行的前一个动作是load的时候,线程T才能对变量V1执行use动作;并且,只有当线程T对变量V1执行的后一个动作是use的时候,线程T才能对变量V1执行load动作。线程T对变量V1的use动作可以认为是和线程T对变量V1的load、read动作相关联,必须连续一起出现。这条规则要求在工作内存中,每次使用V1前都必须先从主内存刷新最新的值,用于保证能看见其他线程对变量V1所做的修改后的值。即使用变量:read->load->use
(2) 写可见性
只有当线程T对变量V1执行的前一个动作是assign的时候,线程T才能对变量V1执行store动作;并且,只有当线程T对变量V1执行的后一个动作是store的时候,线程T才能对变量V1执行assign动作。线程T对变量V1的assign动作可以认为是和线程T对变量V1的store、write动作相关联,必须连续一起出现。这条规则要求在工作内存中,每次修改V1后都必须立刻同步回主内存中,用于保证其他线程可以看到自己对变量V1所做的修改。即修改变量:assign->store->write
(3) 禁止指令重排序
假定动作A是线程T对变量V1实施的use或assign动作,假定动作F是和动作A相关联的load或store动作,假定动作P是和动作F相应的对变量V1的read或write动作;类似的,假定动作B是线程T对变量V2实施的use或assign动作,假定动作G是和动作B相关联的load或store动作,假定动作Q是和动作G相应的对变量V2的read或write动作。如果A先于B,那么P先于Q。这条规则要求volatile修饰的变量不会被指令重排序优化,保证代码的执行顺序与程序的顺序相同
(4) long和double变量的非原子协定
Java 内存模型要求lock、unlock、read、load、assign、use、store和write这8个操作都具有原子性,但是对于64位的数据类型 long 和 double,在模型中特别定义了一条宽松的规定:允许虚拟机将没有被volatile修饰的64位类型是long和double的数据读写操作划分为两次32位的操作来进行。这就是所谓的 long 和 double 的非原子性协定(Nonatomic Treatment of double and long Variables)
尽管long和double具有“非原子协定”,但是大多数虚拟机都将其操作“原子化”。因此,在编码时,无需特别将long和double变量声明为volatile。

volatile变量、普通变量和锁

volatile变量读操作性能与普通变量无异,写操作性能会慢一些,因为需要在本地代码中插入“内存屏障指令”来保证处理器不发生乱序执行
volatile变量对总开销要比锁低。如果一个场景既能使用锁也能使用volatile变量,则尽量使用volatile变量。

volatile使用场景

编写无锁的代码,提高性能。如优化因synchronized滥用导致的性能问题。

参考

《Java编程思想》(第四版) Bruce Eckel [译]陈昊鹏
《java并发编程实战》 Brian Goetz 等著 童云兰 等译
https://www.baidu.com/ 百度AI搜索
https://blog.csdn.net/u012723673/article/details/80682208 Java volatile关键字最全总结:原理剖析与实例讲解
https://juejin.cn/post/6844903520760496141 面试官最爱的volatile关键字

你可能感兴趣的:(#,Java并发编程,Java,java,jvm,volatile,关键字,内存模型,同步)