Java底层角度聊聊volatile关键字原理

简介

volatile是Java提供的一种轻量级的同步机制。Java 语言包含两种内在的同步机制:同步块(或方法)和 volatile 变量,相比于synchronized(synchronized通常称为重量级锁),volatile更轻量级,因为它不会引起线程上下文的切换和调度。但是volatile 变量的同步性较差(有时它更简单并且开销更低),而且其使用也更容易出错。

Java内存模型的特性

  1. 原子性:同一时刻仅可有一个线程可以执行变更;
  2. 有序性:程序的执行按照代码的先后顺序执行
  3. 可见性:多线程环境下,一个共享变量的变更,所有线程可以立即得到变更后的值  

                      多线程环境下,一个线程对共享变量的变更,其他线程是不可见的。Java提供了volatile来保证可见性,当一个变量被volatile修饰后,表示线程的工作内存无效,当一个线程修改共享变量后,其他线程中的工作内存变量会失效,使用时需要直接从主内存中读取。当然,synchronize和Lock都可以保证可见性。synchronized和Lock能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中。因此可以保证可见性。

volatile变量的特性

(1)保证可见性,不保证原子性

       a.当写一个volatile变量时,JMM会把该线程本地内存中的变量强制刷新到主内存中去;

       b.这个写会操作会导致其他线程中的缓存无效。

(2)禁止指令重排

重排序是指编译器和处理器为了优化程序性能而对指令序列进行排序的一种手段。重排序需要遵守一定规则:

  a.重排序操作不会对存在数据依赖关系的操作进行重排序。

  比如:a=1;b=a; 这个指令序列,由于第二个操作依赖于第一个操作,所以在编译时和处理器运

行时这两个操作不会被重排序。

     b.重排序是为了优化性能,但是不管怎么重排序,单线程下程序的执行结果不能被改变

  比如:a=1;b=2;c=a+b这三个操作,第一步(a=1)和第二步(b=2)由于不存在数据依赖关系, 所以可能会发生重排序,但是c=a+b这个操作是不会被重排序的,因为需要保证最终的结果一定是c=a+b=3。

重排序在单线程下一定能保证结果的正确性,但是在多线程环境下,可能发生重排序,影响结果,下例中的1和2由于不存在数据依赖关系,则有可能会被重排序,先执行status=true再执行a=2。而此时线程B会顺利到达4处,而线程A中a=2这个操作还未被执行,所以b=a+1的结果也有可能依然等于2。

即执行到volatile变量时,其前面的所有语句都执行完,后面所有语句都未执行。且前面语句的结果对volatile变量及其后面语句可见。因为运行时依靠CPU屏障(happen-before)指令来阻止重排。

volatile原理

volatile可以保证线程可见性且提供了一定的有序性,但是无法保证原子性。在JVM底层volatile是采用“内存屏障”来实现的。观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令,lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:

I. 它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;

II. 它会强制将对缓存的修改操作立即写入主存;

III. 如果是写操作,它会导致其他CPU中对应的缓存行无效。

单例模式的双重锁为什么要加volatile

Java底层角度聊聊volatile关键字原理

需要volatile关键字的原因是,在并发情况下,如果没有volatile关键字,在第5行会出现问题。instance = new TestInstance();可以分解为3行伪代码

a.memory = allocate() //分配内存

b. ctorInstanc(memory) //初始化对象

c. instance = memory //设置instance指向刚分配的地址

上面的代码在编译运行时,可能会出现重排序从a-b-c排序为a-c-b。在多线程的情况下会出现以下问题。当线程A在执行第5行代码时,B线程进来执行到第2行代码。假设此时A执行的过程中发生了指令重排序,即先执行了a和c,没有执行b。那么由于A线程执行了c导致instance指向了一段地址,所以B线程判断instance不为null,会直接跳到第6行并返回一个未初始化的对象。