一、volatile
volatile
是Java
虚拟机提供的轻量级的同步机制,它有3个特性:
1)保证可见性
2)不保证原子性
3)禁止指令重排
- 当写一个
volatile
变量时,JMM
会把该线程对应的本地内存中的共享变量值立即刷新回主内存中 - 当读一个
volatile
变量时,JMM
会把该线程对应的本地内存设置为无效,直接从主内存中读取共享变量
所以volatile
的写内存语义是直接刷新到主内存中,读的内存语义是直接从主内存中读取
1. 可见性
保证不同线程对这个变量进行操作时的可见性,即变量一旦改变所有线程立即可见 。
使用volatile
修饰共享变量,被volatile
修改的变量有以下特点:
- 线程中读取的时候,每次读取都会去主内存中读取共享变量最新的值,然后将其复制到工作内存
- 线程中修改了工作内存中变量的副本,修改之后会立即刷新到主内存
从volatile
变量的读写过程分析:
要
use
(使用)一个变量的时候必需load
(载入),要载入的时候必需从主内存read
(读取)这样就解决了读的可见性。写操作是把assign
(赋值)和store
(存储)做了关联(在assign
(赋值)后必需store
(存储)),store
(存储)后write
(写入)。 也就是做到了给一个变量赋值的时候一串关联指令直接把变量值写到主内存。 就这样通过用的时候直接从主内存取,在赋值到直接写回主内存做到了内存可见性。
2. 无原子性
原子性指的是一个操作是不可中断的,即使是在多线程环境下,一个操作一旦开始就不会被其他线程影响。
多线程环境下,“数据计算”和“数据赋值”操作可能多次出现,即操作非原子。若数据在加载之后,若主内存count
变量发生修改之后,由于线程工作内存中的值在此前已经加载,从而不会对变更操作做出相应变化,即私有内存和公共内存中变量不同步,进而导致数据不一致。
对于volatile
变量,JVM
只是保证从主内存加载到线程工作内存的值是最新的,也就是数据加载时是最新的。由此可见volatile
解决的是变量读时的可见性问题,但无法保证原子性,对于多线程修改共享变量的场景必须使用加锁同步。
以i++
为例,不具备原子性,该操作是先读取值,然后写回一个新值,相当于原来的值加上1
,分3
步完成。
如果第二个线程在第一个线程读取旧值和写回新值期间(上图所指三步期间)读取i
的域值,那么第二个线程就会与第一个线程一起看到同一个值,并执行相同值的加1
操作,这也就造成了线程安全失败,因此对于add
方法必须使用synchronized
修饰,以便保证线程安全。
从volatile
变量的读写过程分析:
read-load-use
和assign-store-write
成为了两个不可分割的原子操作,但是在use
和assign
之间依然有极小的一段真空期,有可能变量会被其他线程读取,导致写丢失一次
3. 指令禁重排
重排序:
是指编译器和处理器为了优化程序性能而对指令序列进行重新排序的一种手段,有时候会改变程序语句的先后顺序。
- 不存在数据依赖关系,可以重排序;
- 存在数据依赖关系,禁止重排序 。
但重排后的指令绝对不能改变原有的串行语义。
数据依赖性:若两个操作访问同一变量,且这两个操作中有一个为写操作,此时两操作间就存在数据依赖性
重排序的分类和执行流程 :
-
编译器优化的重排序:编译器在不改变单线程串行语义的前提下,可以重新调整指令的执行顺序
-
指令级并行的重排序:处理器使用指令级并行技术来将多条指令重叠执行,若不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序
-
内存系统的重排序:由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是乱序执行
volatile
有关禁重排的行为:
-
当第一个操作为
volatile
读时,不论第二个操作是什么,都不能重排序。这个操作保证了volatile
读之后的操作不会被重排到volatile
读之前(volatile
读之后的操作,都禁止重排序到volatile
之前) -
当第二个操作为
volatile
写时,不论第一个操作是什么,都不能重排序。这个操作保证了volatile
写之前的操作不会被重排到volatile
写之后 (volatile
写之前的操作,都禁止重排序到volatile
之后) -
当第一个操作为
volatile
写时,第二个操作为volatile
读时,不能重排。(volatile
写之后volatile
读,禁止重排序的)
内存屏障四大指令插入情况:
-
在每个
volatile
写操作的前面插入一个StoreStore
屏障,保证在volatile
写之前,其前面的所有普通写操作都已经刷新到主内存中。 -
在每个
volatile
写操作的后面插入一个StoreLoad
屏障,避免volatile
写与后面可能有的volatile
读/写操作重排序 -
在每个
volatile
读操作的后面插入一个LoadLoad
屏障,禁止处理器把上面的volatile
读与下面的普通读重排序。 -
在每个
volatile
读操作的后面插入一个LoadStore
屏障,禁止处理器把上面的volatile
读与下面的普通写重排序。
二、如何正确使用volatile
由于volatile
变量只能保证可见性,在不符合以下两条规则的运算场景中,我们仍然要通过加锁(使用synchronized
、java.util.concurrent
中的锁或原子类)来保证原子性:
- 运算结果并不依赖变量的当前值,或者能够确保只有单一的线程修改变量的值。
- 变量不需要与其他的状态变量共同参与不变约束。
1. 单一赋值可以,但是含复合运算赋值不可以(i++
之类)
volatile int a = 10;
2. 状态标志,判断业务是否结束
使用:作为一个布尔状态标志,用于指示发生了一个重要的一次性事件,例如完成初始化或任务结束
理由:状态标志并不依赖于程序内任何其他状态,且通常只有一种状态转换
例子:判断业务是否结束
volatile boolean flag = false
3. 开销较低的读,写锁策略
使用:当读远多于写,结合使用内部锁和volatile
变量来成少同步的开销 。
理由:
- 利用
volatile
保证读取操作的可见性; - 利用
synchronized
保证复合操作的原子性。
4. DCL双端锁的发布
隐患:多线程环境下,由于重排序,该对象可能还未完成初始化就被其他线程读取
修正方法1:加volatile
(也即正确的DCL双端锁)
原理:利用volatile
, 禁止"初始化对象"和"设置singleton
指向内存空间"的重排序
修正方法2:静态内部类