官方定义
This means that changes to a volatile variable are always visible to other threads. What‘s more, it also means that when a thread reads a volatile variable, it sees not just the latest change to the volatile, but also the side effects of the code that led up the change.
官方的描述只看懂了,前半部分,意思是,volatile 修饰的变量总是对其他线程是可见的,后半部分理解不了,不过没关系我们继续探索。
什么是可见性?
说到可见性需要对 JMM 模型 和 硬件内存架构 有所了解
可以看一下 JMM 和 硬件内存架构 的内容
上图就是 现代硬件内存架构,我们围绕这个来讨论可见性
- JVM 线程是实实在在的绑定了 OS 线程的,即我们通过代码 Thread.start() 就会向 OS 申请并绑定一个OS线程
- 由于CPU 寄存器上的执行速度远大于主存中的读写速度,所以需要在两者之间添加高速缓存,提高 CPU 利用率
- CPU 计算的流程如下
- 当 CPU 切换到某个线程,而且改线程需要进行运算 i = i+1
- 先将需要的 i 从主存加载到高速缓存
- CPU 寄存器会 复制 i 的值 到 CPU 寄存器中
- CPU 寄存器自增加 1
- 将结果赋值给高速缓存层 中的 i
- 高速缓存层在刷回到主存中
如果在单线程环境下没有问题,但是在多线程环境下,就会导致一个问题,在不同的线程中高速缓存中 i 的值会出现不一致,
即 在线程 t1 计算完 +1 操作但是还没刷回主存这段时间之前 t2 获取到值还是原来的 i
所以就有了 volatile ,它的作用是 如果线程 t1修改了 i,会马上刷回主存,并且其他线程中高速缓存中的 i 值失效,需要重新从主存中加载,这样就所有线程中的高速缓存 i 值都是一样的
一句话概括
volatile 修饰的变量就是让所有线程中的高速缓存中该变量都是相同值
volatile 等于 原子性吗?
我们换个说法,就是volatile 修饰的变量线程安全吗? 答案是否定的!
我们注意思考上的概括,说的是高速缓存中的变量是相同的,没有说已经被寄存器复制的变量副本!!
假设 i == 1 在主存中,现在需要对 i 自增 1 两次,我们采用了两个线程来运行,时间顺序如下
- t1 和 t2 线程启动
- t1,线程开始运行,从主存加载数据到高速缓存,寄存器从高速缓存复制 i的值 1 到寄存器自加1,这时 i == 2
- 寄存器将值赋给t1 线程中的高速缓存i,这时高速缓存的 i值为 2
- 这时 t1 进行了上下文切换,切换到了其他线程
- t2 线程开始被另一个CPU 核 加载
- t2 线程开始运行,从主存加载数据到高速缓存,寄存器从高速缓存复制 i的值 1 到寄存器,这时寄存器的值是 1
- t2 被其他线程切换
- t1 被调度,操作系统恢复了刚刚 t1 的状态和数据,由于 i是被volatile 修饰,需要刷回主存
- 这时 主存中 的 i == 2
- t2 被调度操作系统恢复了刚刚 t2 的状态和数据(高速缓存 i == 1,寄存器中的值==1)
- 由于 i是被volatile 修饰,所以高速缓存中的失效了,需要从主存加载,加载后的 t2 数据(高速缓存 i == 2,寄存器中的值==1)
- t2 继续运行,自加 1 ,赋值给 t2 的高速缓存变量 i == 2
- t2 将变量刷回主存,i == 2
好的,看到问题了,自加了两次的结果等于2 线程不安全!!
那 volatile 有什么用?
可见性并不能解决线程安全问题啊??,加了还不如不加?
其实不然
- volatile & cas 就能解决线程安全问题(AtomicInteger 类就是例子)
- volatile 的可见性,如有改动需要马上刷回主存,这个可以用来做线程间的通信
- volatile对指令重排的影响
问题
Java 多线程在单核CPU,volatile 是否还有必要呢?
如果高速缓存区多个线程共享
- 那么volatile的内存可见特性就显得无关紧要——因为不同线程无需通过主内存进行通信
- volatile对指令重排的影响,确保volatile的写-读和监视器的释放-获取一样,具有相同的内存语义
参考