volatile是Java提供的一种轻量级的同步机制。Java 语言包含两种内在的同步机制:同步块(或方法)和 volatile 变量,相比于synchronized(synchronized通常称为重量级锁),volatile更轻量级,因为它不会引起线程上下文的切换和调度。但是volatile 变量的同步性较差(有时它更简单并且开销更低),而且其使用也更容易出错。
volatile如何实现同步机制呢?
一、工作内存与主内存
主内存:计算机物理内存的一部分,在JVM运行时数据区中,主要指得是堆存储对象实例的部分
工作内存:每条线程有自己的工作内存,处理器的高速缓存,主要存储线程的局部变量、方法变量、线程用到的主内存对象副本拷贝(并不是全部拷贝,直拷贝线程用到的对象字段)
lock(锁定):作用于主内存的变量,它把一个变量标识为一条线程独占的状态。
read(读取):作用于主内存的变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用。
load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中。
use(使用):作用于工作内存的变量,它把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用到变量的值的字节码指令时将会执行这个操作。
assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
store(存储):作用于工作内存的变量,它把工作内存中一个变量的值传送到主内存中,以便随后的write操作使用。
write(写入):作用于主内存的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中。
unlock(解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
如果要把一个变量从主内存复制到工作内存,那就要顺序地执行read和load操作
如果要把变量从工作内存同步回主内存,就要顺序地执行store和write操作。
注意,Java内存模型只要求上述两个操作必须按顺序执行,而没有保证是连续执行。也就是说,read与load之间、store与write之间是可插入其他指令的,如对主内存中的变量a、b进行访问时,一种可能出现顺序是read a、read b、load b、load a。
除此之外,Java内存模型还规定了在执行上述8种基本操作时必须满足如下规则:
1、不允许read和load、store和write操作之一单独出现,即不允许一个变量从主内存读取了但工作内存不接受,或者从工作内存发起回写了但主内存不接受的情况出现。
2、不允许一个线程丢弃它的最近的assign操作,即变量在工作内存中改变了之后必须把该变化同步回主内存。
3、不允许一个线程无原因地(没有发生过任何assign操作)把数据从线程的工作内存同步回主内存中。
4、一个新的变量只能在主内存中“诞生”,不允许在工作内存中直接使用一个未被初始化(load或assign)的变量,换句话说,就是对一个变量实施use、store操作之前,必须先执行
过了assign和load操作。
5、一个变量在同一个时刻只允许一条线程对其进行lock操作,但lock操作可以被同一条线程重复执行多次,多次执行lock后,只有执行相同次数的unlock操作,变量才会被解锁。
6、如果对一个变量执行lock操作,那将会清空工作内存中此变量的值,在执行引擎使用这个变量前,需要重新执行load或assign操作初始化变量的值。
7、如果一个变量事先没有被lock操作锁定,那就不允许对它执行unlock操作,也不允许去unlock一个被其他线程锁定住的变量。
8、对一个变量执行unlock操作之前,必须先把此变量同步回主内存中(执行store、write操作)。
这8种内存访问操作以及上述规则限定,再加上稍后介绍的对volatile的一些特殊规定,就已经完全确定了Java程序中哪些内存访问操作在并发下是安全的。
二、volatile与内存屏障
前面介绍了volatile的可见性和有序性,那JVM到底是如何为volatile关键字实现的这两大特性呢,Java内存模型其实是通过内存屏障(Memory Barrier)来实现的。
内存屏障其实也是一种JVM指令,Java内存模型的重排规则会要求Java编译器在生成JVM指令时插入特定的内存屏障指令,通过这些内存屏障指令来禁止特定的指令重排序。
另外内存屏障还具有一定的语义:内存屏障之前的所有写操作都要回写到主内存,内存屏障之后的所有读操作都能获得内存屏障之前的所有写操作的最新结果(实现了可见性)。因此重排序时,不允许把内存屏障之后的指令重排序到内存屏障之前。
volatile的有序性
有序性是指程序代码的执行是按照代码的实现顺序来按序执行的。
volatile的有序性特性则是指禁止JVM指令重排优化。
我们来看一个例子:
public class Singleton { private static Singleton instance = null; //private static volatile Singleton instance = null; private Singleton() { } public static Singleton getInstance() { //第一次判断 if(instance == null) { synchronized (Singleton.class) { if(instance == null) { //初始化,并非原子操作 instance = new Singleton(); } } } return instance;
上面的代码是一个很常见的单例模式实现方式,但是上述代码在多线程环境下是有问题的。为什么呢,问题出在instance对象的初始化上,因为instance = new Singleton();这个初始化操作并不是原子的,在JVM上会对应下面的几条指令:
memory =allocate(); //1. 分配对象的内存空间 ctorInstance(memory); //2. 初始化对象 instance =memory; //3. 设置instance指向刚分配的内存地址
上面三个指令中,步骤2依赖步骤1,但是步骤3不依赖步骤2,所以JVM可能针对他们进行指令重拍序优化,重排后的指令如下:
memory =allocate(); //1. 分配对象的内存空间 instance =memory; //3. 设置instance指向刚分配的内存地址 ctorInstance(memory); //2. 初始化对象
这样优化之后,内存的初始化被放到了instance分配内存地址的后面,这样的话当线程1执行步骤3这段赋值指令后,刚好有另外一个线程2进入getInstance方法判断instance不为null,这个时候线程2拿到的instance对应的内存其实还未初始化,这个时候拿去使用就会导致出错。
所以我们在用这种方式实现单例模式时,会使用volatile关键字修饰instance变量,这是因为volatile关键字除了可以保证变量可见性之外,还具有防止指令重排序的作用。当用volatile修饰instance之后,JVM执行时就不会对上面提到的初始化指令进行重排序优化,这样也就不会出现多线程安全问题了。
补充:JVM对viloate变量的特殊规则
T表示一个线程,V、W表示两个violate修饰的变量,在执行read、load、use、assign、store、write指令必须满足以下规则
1、T对V执行的前一个动作是load,才能对V执行use操作;在执行下一个动作use的时候,才能执行前一个动作load.(为了保证变量使用之前必须获取变量最新的值,获取其它线程刷新的值)
2、T对V执行的前一个动作是assign的时候,才能对T执行store动作,执行assign之后,必须执行store(保证对变量的修改,立即保存到主内存)
3、禁止对变量的排序优化
部分摘录:https://blog.csdn.net/huyongl1989/article/details/90712393