Java关键字之 volatile

咕咚 于 2015-08-03 发布

Java有众多关键字,volatile作为一个和同步相关的关键字,很少在自己的项目中使用,今天在看Handler源码时无意看到,顺便便了解了下。 记录以备后用。

做了一个实验

更新于:2020/03/28

在主线程创建一个 int 变量,然后开启两个线程 A、B

预期:B 更改完 int,A 应该显示正确的值

update is 1585353251102
------> 1585353251102
------> 1585353251607
update is 1585353251607
------> 1585353251607

实际两个存在不同步的情况,这里当然可以用线程同步的方式的去处理这个问题,但是这里分析原因,为什么现在会存在不同步?

因为 int 值的存在工作内存跟主内存,两者存在时间差, B 线程更新完 int 并没有及时同步,所以 A 就不能及时打印出正确的值。

这时如果使用volatile来修饰 int 值,int 值每次的改变都会从工作内存更新到主内存,这样的更改是内存中彻底的更改。

要解决共享对象可见性这个问题,我们可以使用 java volatile 关键字。 Java’s volatile keyword. volatile 关键字可以保证变量会直接从主存读取,而对变量的更新也会直接写到主存。volatile 原理是基于 CPU 内存屏障指令实现的

全面理解 Java 内存模型_Java_Heaven Wang 的专栏 - CSDN 博客


volatile关键字 可以被看作是一种 “程度较轻的 synchronized”;与 synchronized 块相比,volatile 变量所需的编码较少,并且运行时开销也较少,但是它所能实现的功能也仅是 synchronized 的一部分。

一般的,Java中设置变量的操作,除了long和double类型的变量,其余都是原子操作,也就是说,对于基本的变量设置操作没必要使用同步。

在JVM1.2之前,Java的内存模型,总是从主内存读取变量,就是说,变量只存在于内存的一个地方,但是随着JVM的成熟,读取内存的过程也做了相应的更改。

在当前的内存模型下,线程可以把变量在本地内存(比如机器的寄存器),而不是直接在内存读取,这个变化的原因,我猜测可能是下面的原因: 如果频繁的修改读取内存中的一个变量,可能效率比较低,在JVM1.2之后,做了优化,发现如果一个变量被频繁的读取修改,那么就把这个变量先放到本地寄存器,这个读取效率更高。每次直接去读寄存器中的数据。

但是,现在就出现一个问题,一个线程在主内存修改了变量的值,另一个线程在寄存器修改了这个变量的值,此时就出现了变量值不同步的问题,最终数据不一致。 这时,volatile关键字就出现了。

只要把变量声明为 volatile,这就指示JVM,这个变量是不稳定的,只要成员变量发生变化,强迫线程将变化同步到主内存,一般说来,多任务环境下各任务间共享的标志都应该加volatile修饰。 Volatile修饰的成员变量在每次被线程访问时,都强迫从共享内存中重读该成员变量的值。而且,当成员变量发生变化时,强迫线程将变化值回写到共享内存。这样在任何时刻,两个不同的线程总是看到某个成员变量的同一个值。

坑(来自摘抄)

其实 volatile 这个关键字有两层语义。

第一层语义相信大家都比较熟悉,就是可见性。

可见性指的是在一个线程中对该变量的修改会马上由工作内存(Work Memory)写回主内存(Main Memory),所以会马上反应在其它线程的读取操作中。顺便一提,工作内存和主内存可以近似理解为实际电脑中的高速缓存和主存,工作内存是线程独享的,主存是线程共享的。

volatile的第二层语义是禁止指令重排序优化。

大家知道我们写的代码(尤其是多线程代码),由于编译器优化,在实际执行的时候可能与我们编写的顺序不同。编译器只保证程序执行结果与源代码相同,却不保证实际指令的顺序与源代码相同。这在单线程看起来没什么问题,然而一旦引入多线程,这种乱序就可能导致严重问题。volatile关键字就可以从语义上解决这个问题。

注意,前面反复提到“从语义上讲是没有问题的”,但是很不幸,禁止指令重排优化这条语义直到jdk1.5以后才能正确工作。此前的JDK中即使将变量声明为volatile也无法完全避免重排序所导致的问题。所以,在jdk1.5版本前,双重检查锁形式的单例模式是无法保证线程安全的。

from 你真的会写单例模式吗——Java实现

总结:

volatile关键字就是提示VM:对于这个成员变量不能保存它的私有拷贝,而应直接与共享成员变量交互 使用volatile关键字,就表示着将屏蔽掉JVM对变量访问的优化,所以在效率上比较低,因此一定在必要时才使用此关键字。

摘抄

point1 : Volatile修饰的成员变量在每次被线程访问时,都强迫从共享内存重新读取该成员的值,而且,当成员变量值发生变化时,强迫将变化的值重新写入共享内存,这样两个不同的线程在访问同一个共享变量的值时,始终看到的是同一个值。

point2 : java语言规范指出:为了获取最佳的运行速度,允许线程保留共享变量的副本,当这个线程进入或者离开同步代码块时,才与共享成员变量进行比对,如果有变化再更新共享成员变量。这样当多个线程同时访问一个共享变量时,可能会存在值不同步的现象。