java的volatile关键字和它的底层实现


1. volatile关键字的用法

在java中,我们可以用volatile修饰实例变量或类变量(但不能是final的,也不能是局部变量)。

public class Hello {
private volatile int v1; // OK
private static volatile int v2; // OK
private final volatile int v3; // error: illegal combination of modifiers: final and volatile

public void test() {
volatile int v4 = 5; // error: illegal start of expression
}

public static void main(String[] args) {
System.out.println("Over");
}
}

2. volatile的作用

volatile主要作用有两个,可见性有序性,还有一个额外的作用,保证单个变量的读写操作原子性

2.1. 可见性

  1. 什么是可见性?
    在多核CPU环境下,并发执行的线程之间,共享主存,共享变量保存在主存,一个线程修改了共享变量,其他线程可以读到修改后的共享变量的值。
    这里共享变量包含类的实例变量,类的静态变量。

  2. 为什么有可见性问题?
    现代的CPU处理速度比内存读写速度快很多,然后为了提供计算机处理速度,在CPU和内存之间添加了高速缓存(例如:一级缓存、二级缓存、三级缓存等)。
    多核CPU中每个处理器都有自己的缓存,然后共享主存。这样就会有缓存一致性问题,缓存中的值跟主存中的值可能不一致,这也就是可见性问题。

cpu-cache

  1. 怎么保证的可见性?
    通过lock前缀指令,这个指令会做两件事:
    a. 将当前CPU缓存行的数据写回主存
    b. 写回主存的操作使得其他CPU缓存了该内存地址的数据无效 【这里用到了MESI缓存一致性协议】

2.2. 有序性

  1. 什么是有序性问题?
    CPU在执行java的代码时,最底层的指令经过了编译器,处理器以及内存系统优化后,它的顺序发生了变化,以加快指令的执行。
    这种优化在单线程下,没有问题。但是在多线程环境下,可能会造成空指针,数据异常等情况。

  2. 举例说明有序性问题
    单例的一种写法是通过synchronized同步块,加上两次null检查来实现,这时候需要对静态变量加上volatile修饰,原因是new对象有三步操作:
    a. 申请内存空间
    b. 调用构造函数
    c. 返回内存地址赋值给引用变量
    处理器可能对这三步执行顺序做优化,比如申请内存地址后,直接返回内存地址赋值给引用,最后再调用构造函数进行初始化,这样其他的线程可能拿到了未初始化的单例对象。

// 单例对象的double check写法
public class Singleton {
private static volatile Singleton instance;

private Singleton() {}

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

  1. 怎么解决的?
    对volatile变量的读写操作前后插入内存屏障来解决。

3. volatile的底层实现

3.1. Java内存模型 (JMM)

针对主存,CPU缓存,Java语言规范提出语言级别的内存模型 (JMM),它能屏蔽不同的编译器和不同处理器平台的差异,通过插入内存屏障指令,
禁止编译器重排序和处理器重排序,为程序员提供一致的内存可见性保证。

jmm-fig

3.2. 内存屏障 — java语言角度

  1. 内存屏障的两个作用
    a) 在有内存屏障的地方,会禁止指令重排序,也就是屏障下面的代码不能跟屏障上面的代码交换执行顺序
    b) 在有内存屏障的地方,线程修改完共享变量后,会马上将该变量从本地内存写会主存,并且让其他线程的本地内存中该变量副本失效。

  2. 它与Lock前缀指令的关系?
    Lock前缀指令不是一种内存屏障,但是它能完成类似内存屏障的功能。Lock前缀指令会对CPU总线和高速缓存加锁,可以理解为CPU指令级别的一种锁。
    内存屏障不一定就是用Lock前缀指令来实现的,也可以用其他指令(例如:fences)来实现。

3.3. 缓存一致性协议 — 硬件的角度

Intel处理器使用的是MESI协议,即修改(modify), 独占(eclusive),共享(share), 无效(invalid)。
处理器使用嗅探技术保证内部缓存、系统内存和其他处理器的的缓存数据在总线上保持一致。

该协议会组织同时修改两个以上处理器缓存的内存区域,具体实现是,处理器执行指令期间,需要独占任何共享内存,以前是锁总线,现在是锁缓存。

8-atom-directive

4. 参考

Java 内存模型-同步八种操作_timchen525的专栏-CSDN博客


文章作者: IT神助攻
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 IT神助攻 !
  目录