volatile关键字
概要
volatile修饰符并不是Java语言的首创,早在C和C++当中就已经存在。为了理解volatile关键字的作用和原理,需要先了解一些计算机基础知识。请先参考《什么是Java内存模型(JMM)?》
我们知道,并发编程时,线程安全涉及三个特性:原子性、可见性、有序性。volatile用于保证修饰变量的可见性、有序性,但是不能保证原子性。
一、 可见性
当访问共享变量的多个线程运行在多核CPU上时,可能会出现可见性问题。synchronized关键字和lock可以解决这个问题,但是会阻塞线程,降低性能,所以java给出了更轻量级关键字volatile,不会阻塞线程。
1. volatile 可见性实现
保证变量在线程间的可见性。可见性的保证是基于CPU的内存屏障指令,被JSR-133抽象为happens-before原则。
volatile 变量的内存可见性是基于内存屏障(Memory Barrier)实现
内存屏障:
内存屏障也称为内存栅栏或栅栏指令,是一种屏障指令,它使CPU或编译器对屏障指令之前和之后发出的内存操作执行一个排序约束。 这通常意味着在屏障之前发布的操作被保证在屏障之后发布的操作之前执行。
1. 这里的可见性是什么意思?
当一个线程修改了变量的值,新的值会立刻同步到主内存当中。而其他线程读取这个变量的时候,也会从主内存中拉取最新的变量值。
volatile关键字有这样的特性得益于java语言的先行发生原则(happens-before)
在计算机科学中,先行发生原则是两个事件的结果之间的关系,如果一个事件发生在另一个事件之前,结果必须反映,即使这些事件实际上是乱序执行的(通常是优化程序流程)
这里所谓的事件,实际上就是各种指令操作,比如读操作、写操作、初始化操作、锁操作等等。
先行发生原则作用于很多场景下,包括同步锁、线程启动、线程终止、volatile。
我们这里只列举出volatile相关的规则:对于一个volatile变量的写操作先行发生于后面对这个变量的读操作。
二、有序性
保证有序性,阻止指令重排。编译时JVM编译器遵循内存屏障的约束,运行时依靠CPU屏障指令来阻止重排。
1. 什么是指令重排
指令重排是指JVM在编译Java代码的时候,或者CPU在执行JVM字节码的时候,对现有的指令顺序进行重新排序。简单来说就是系统在执行代码的时候并不一定是按照代码顺序依次执行。
2. 指令重排的目的
指令重排的目的是为了在不改变程序执行结果的前提下,优化程序的运行效率。需要注意的是,这里所说的不改变执行结果,指的是不改变单线程下的程序执行结果。
Java 源代码会经历 编译器优化重排 —> 指令并行重排 —> 内存系统重排 的过程,最终才变成操作系统可执行的指令序列。指令重排序可以保证串行语义一致,但是没有义务保证多线程间的语义也一致 ,所以在多线程下,指令重排序可能会导致一些问题。
内存屏障共分为四种类型:
LoadLoad屏障:
抽象场景:Load1; LoadLoad; Load2
Load1 和 Load2 代表两条读取指令。在Load2要读取的数据被访问前,保证Load1要读取的数据被读取完毕。
说明:禁止下面所有的普通读操作和上面的 volatile 读重排序。
StoreStore屏障:
抽象场景:Store1; StoreStore; Store2
Store1 和 Store2代表两条写入指令。在Store2写入执行前,保证Store1的写入操作对其它处理器可见
说明:禁止上面的普通写和下面的 volatile 写重排序。
由于StoreStore屏障保障上面所有的普通写在volatile写之前刷新到主内存,StoreStore屏障可以保证在volaitle写之前,其前面的所有普通写操作已经对任意处理器可见了。
LoadStore屏障:
抽象场景:Load1; LoadStore; Store2
在Store2被写入前,保证Load1要读取的数据被读取完毕。
说明:禁止下面所有的普通写操作和上面的 volatile 读重排序。
StoreLoad屏障:
抽象场景:Store1; StoreLoad; Load2
在Load2读取操作执行前,保证Store1的写入对所有处理器可见。StoreLoad屏障的开销是四种屏障中最大的。
说明:防止上面的 volatile 写与下面可能有的 volatile 读/写重排序。
在一个变量被volatile修饰后,JVM会为我们做两件事:
1.在每个volatile写操作前插入StoreStore屏障,在写操作后插入StoreLoad屏障。
2.在每个volatile读操作前插入LoadLoad屏障,在读操作后插入LoadStore屏障。
三、解决了long类型和double类型数据的8字节赋值问题
volatile除了保证可见性和阻止指令重排,还解决了long类型和double类型数据的8字节赋值问题。
在Java中,long类型和double类型的赋值不是原子操作,它们的赋值过程需要分两步完成:先将高32位写入内存,再将低32位写入内存。这种情况下,如果一个线程在赋值的过程中被另一个线程打断,可能会导致某个线程读取到了部分更新后的值,而另一部分还是旧值,从而出现数据不一致的情况。
四、volatile使用场景
1.运行结果并不依赖变量的当前值,或者能够确保只有单一的线程修改变量的值。
2.变量不需要与其他的状态变量共同参与不变约束。
五、volatile的相关总结
1. volatile是轻量级同步机制
在访问volatile变量时不会执行加锁操作,因此也就不会使执行线程阻塞,是一种比synchronized关键字更轻量级的同步机制
2. 不能替代synchronized和加锁机制
volatile只能保证内存可见性,不能保证原子性,所以不能替代synchronized和加锁机制。后两种机制既可以确保可见性又可以确保原子性。
3. 效率比较低
volatile频繁从内存中读写,且屏蔽掉了JVM中必要的代码优化,和普通变量比较,效率上比较低,因此一定在必要时才使用此关键字
标签:指令,关键字,屏障,线程,内存,操作,volatile From: https://www.cnblogs.com/hld123/p/18178674