image.png

1.volatile是什么

volatile是Java虚拟机提供的轻量级的同步机制
保证可见性和禁止指令重排

2.JMM

JMM(Java内存模型 Java Memory Model,,简称JMM)本身是一种抽象的概念并不真实存在, 它描述的是一组规则或规范,通过这组规范定义了程序中各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式。

JMM关于同步的规定:

  1. 线程解锁前,必须把共亨变量的值刷新回主内存
  2. 线程加锁前,必须读取主内存的最新值到自己的工作内存
  3. 加锁解锁是同一把锁

由于JVM运行程序的实体是线程, 而每个线程创建时JVM都会为其创建一个工作内存(有些地方称为栈空间),工作内存是每个线程的私有数据区域, 而Java内存模型中规定所有变量都存储在主内存, 主内存是共享内存区域,所有线程都可以访问, 但线程对变量的操作(读取赋值等)必须在工作内存中进行,首要将变量从主内存拷贝的自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写回主內存, 不能直接操作主内存中的变量,各个线程中的工作内存中存储着主内存中的变量副本拷贝,因此不同的线程间无法访问对方的工作内存,线程间的通信(传值)必须通过主内存来完成, 其简要访问过程如下图
image.png

有序性

计算机在执行程序时,为了提髙性能,编译器和处理器的常常会对指令做重排, 一般分以下3种
image.png

单线程环境里面确保程序最终执行结果和代码顺序执行的结果一致。

处理器在进行重排序时必须要考虑指令之间的数据依顿性

多线程环境中线程交替执行,由于编译器优化重排的存在,两个线程中使用的变量能否保证一致性是无法确定的,结果无法预测

指令重排示例1
image.png

指令重排示例2
image.png

指令重排示例3

  1. public class ReSortseqDemo {
  2. boolean flag = false;
  3. int a = 0;
  4. public void method01(){
  5. a = 1; //语句1
  6. flag = true; //语句2
  7. }
  8. //多线程环埯中线程交替执行, 由于编译器优化重排的存在,
  9. //两个线程中使用的变量能否保证一致性是无法确定的,结果无法预测
  10. public void method02{
  11. if(flag){
  12. a = a + 5; //语句3
  13. System.out.println("*****retValue: " + a);
  14. //第一种情况,a 输出6, 语句1执行完
  15. //第二种情况,a 输出5, 语句1没有执行
  16. }
  17. }
  18. }

volatile实现禁止指令重排优化, 从而避免多线程环境下程序岀现乱序执行的现象先了解一个概念,内存屏障( Memory Barrier)又称内存栅栏,是一个CPU指令,它的作用有两个:
一是保证特定操作的执行顺序,
二是保证某些变量的内存可见性(利用该特性实现 volatile的内存可见性)。
由于编译器和处理器都能执行指令重排优化。如果在指令间插入一条 Memory Barrier则会告诉编译器和CPU, 不管什么指令都不能和这条 Memory Barrier指令重排序,也就是说通过插入内存屏障禁止在肉存屏障前后的指令执行重排序优化。内存屏障另外一个作用是强制刷出各种CPU的缓存数据,因此任何CPU上的线程都能读取到这些数据的最新版本。
image.png

线程安全性获得保证

工作内存与主内存同步延迟现象导致的可见性问题
可以使用 synchronized或 volatile关键字解决,它们都可以使一个线程修改后的变量立即对其他线程可见。

对于指令重排导致的可见性问题和有序性问题
可以利用 volatile关键字解决, 因为 volatile的另外一个作就是禁止重排序优化。

3.volatile常见场景

1.双重检查锁的单例模式

class SingletonDemo{
    private static volatile SingletonDemo instance;
    private SingletonDemo(){}

    public static SingletonDemo getInstance(){
       if (instance == null){
           synchronized(SingletonDemo.class){
               if (instance == null){
                   instance = new SingletonDemo();
               }
           }
       }
       return instance;
    }
}

DCL(双端检锁)机制不一定线程安全, 原因是有指令重排序的存在, 而加入 volatile可以禁止指令重排

原因在于某一个线程执行到第一次检测,读取到的 instance不为null时, instance的引用对象可能没有完成初始化
instance= new SingletonDemo(); 可以分为以下3步完成(伪代码)

memory= allocate(); //1.分配对象内存空间 instance( memory); //2.初始化对象 instance= memory; //3.设置 instance指向刚分配的内存地址,此时 instance != null

步骤2和步骤3不存在数据依赖关系,而且无论重排前还是重排后程序的执行结果在单线程中并没有改变,
因此这种重排优化是允许的。

memory= allocate(); //1.分配对象内存空间 instance= memory; //3.设置 instance指向刚分配的内存地址,此时 Instance!=null,但是对象还没有初始化完成! Instance(memor); //2.初始化对象

但是指令重排只会保证串行语义的执行的一致性(单线程),但并不会关心多线程间的语义一致性。
所以当一条线程访问 instance不为null时, 由于 instance实例未必已初始化完成,也成了线程安全问题。