JUC包的两大支柱之volatile

CAS在JUC包中所处的位置


volatile两大作用

一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:1)保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。

2)禁止进行指令重排序。

需注意volatile并不保证操作的原子性。

(一)内存可见性

1 概念

JVM内存模型(JMM):主内存和线程独立的工作内存

java内存模型

Java内存模型规定,对于多个线程共享的变量,存储在主内存当中,每个线程都有自己独立的工作内存(比如CPU的寄存器、CPU缓存),线程只能访问自己的工作内存,不可以访问其它线程的工作内存。

工作内存中保存了主内存共享变量的副本,线程要操作这些共享变量,只能通过操作工作内存中的副本来实现,操作完毕之后再同步回到主内存当中。

如何保证多个线程操作主内存的数据完整性是一个难题,Java内存模型也规定了工作内存与主内存之间交互的协议,定义了8种原子操作:

(1) lock:将主内存中的变量锁定,为一个线程所独占

(2) unclock:将lock加的锁定解除,此时其它的线程可以有机会访问此变量

(3) read:将主内存中的变量值读到工作内存当中

(4) load:将read读取的值保存到工作内存中的变量副本中。

(5) use:将值传递给线程的代码执行引擎

(6) assign:将执行引擎处理返回的值重新赋值给变量副本

(7) store:将变量副本的值存储到主内存中。

(8) write:将store存储的值写入到主内存的共享变量当中。

通过上面Java内存模型的概述,我们会注意到这么一个问题,每个线程在获取锁之后会在自己的工作内存来操作共享变量,操作完成之后将工作内存中的副本回写到主内存,并且在其它线程从主内存将变量同步回自己的工作内存之前,共享变量的改变对其是不可见的。即其他线程的本地内存中的变量已经是过时的,并不是更新后的值。


(二)指令重排

1 概念

 在执行程序时,为了提高性能,编译器和处理器常常会对指令做重排序。重排序分3种类型:编译器优化的重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。指令级并行的重排序。现代处理器采用了指令级并行技术(Instruction-LevelParallelism,ILP)来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。内存系统的重排序。由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。

 从Java源代码到最终实际执行的指令序列,会分别经历下面3种重排序: 

Java指令重排序

对于编译器,JMM的编译器重排序规则会禁止特定类型的编译器重排序(不是所有的编译器重排序都要禁止)。对于处理器重排序,JMM的处理器重排序规则会要求Java编译器在生成指令序列时,插入特定类型的内存屏障(Memory Barriers,Intel称之为Memory Fence)指令,通过内存屏障指令来禁止特定类型的处理器重排序。JMM属于语言级的内存模型,它确保在不同的编译器和不同的处理器平台之上,通过禁止特定类型的编译器重排序和处理器重排序,为程序员提供一致的内存可见性保证。

指令重排序是JVM为了优化指令,提高程序运行效率,在不影响单线程程序执行结果的前提下,尽可能地提高并行度。编译器、处理器也遵循这样一个目标。注意是单线程。多线程的情况下指令重排序就会给程序员带来问题。

不同的指令间可能存在数据依赖。比如下面计算圆的面积的语句:

double r = 2.3d;//(1)

double pi =3.1415926; //(2)

double area = pi* r * r; //(3)

area的计算依赖于r与pi两个变量的赋值指令。而r与pi无依赖关系。

as-if-serial语义是指:不管如何重排序(编译器与处理器为了提高并行度),(单线程)程序的结果不能被改变。这是编译器、Runtime、处理器必须遵守的语义。

虽然,(1) – happensbefore -> (2),(2) – happens before -> (3),但是计算顺序(1)(2)(3)与(2)(1)(3) 对于r、pi、area变量的结果并无区别。编译器、Runtime在优化时可以根据情况重排序(1)与(2),而丝毫不影响程序的结果。

指令重排序包括编译器重排序和运行时重排序。

2 指令重排带来的问题

如果一个操作不是原子的,就会给JVM留下重排的机会。下面看几个例子:

例子1:A线程指令重排导致B线程出错

对于在同一个线程内,这样的改变是不会对逻辑产生影响的,但是在多线程的情况下指令重排序会带来问题。看下面这个情景:

在线程A中:

context = loadContext();

inited = true;

在线程B中:

while(!inited ){ //根据线程A中对inited变量的修改决定是否使用context变量

   sleep(100);

}

doSomethingwithconfig(context);

假设线程A中发生了指令重排序:

inited = true;

context = loadContext();

那么B中很可能就会拿到一个尚未初始化或尚未初始化完成的context,从而引发程序错误。

例子2:指令重排导致单例模式失效

我们都知道一个经典的懒加载方式的双重判断单例模式:

public class Singleton {

  private static Singleton instance = null;

  private Singleton() { }

  public static Singleton getInstance() {

     if(instance == null) {

        synchronzied(Singleton.class) {

           if(instance == null) {

               instance = new Singleton();  //非原子操作

           }

        }

     }

     return instance;

   }

}

看似简单的一段赋值语句:instance= new Singleton(),但是很不幸它并不是一个原子操作,其实际上可以抽象为下面几条JVM指令:

memory =allocate();    //1: