目录
volatile是java的三个关键字之一,实现了可见性和有序性。
其可见性是基于java内存模型JMM实现的,原理为写操作之后将线程的本地内存刷新到主内存,读操作将本地内存的缓存置为无效;
至于其有序性是通过控制编译器和处理器的指令重排序实现的:对于volatile变量的写,happends-before于对此变量的读。
volatile定义
Java编程语言允许线程访问共享变量,为了确保共享变量能被准确和一致地更新,线程应该确保通过排他锁单独获得这个变量。
在多线程编程中synchronize和volatile都扮演着重要的角色,volatile是轻量级的synchronize,在某些情况下比锁更方便。
Java并发编程出现问题的根源就在于三个方面:
- 原子性:一个或者一系列操作是不可被中断的(java中的实现有:锁、循环CAS、JUC中提供的并发类)
- 可见性:当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值
- 有序性:通过禁止进某种类型的重排序,让一个操作执行的结果对另一个操作可见;(有的资料中也会统一地称此特性为可见性)
volatile关键字实现了其中的:可见性、有序性。
volatile——可见性
CPU级别volatile可见性
例如:
instance = new Singleton();//instance是volatile变量
将其转变为汇编码:
0x01a3de1d: movb $0x0,0x1104800(%esi);0x01a3de24: lock add1 $0x0,(%esp);
当有 volatile修饰的变量进行写操作 时,将会多出一行LOCK指令的编码,这个指令所做的事情:
- 将当前处理器缓存行的数据写回到系统内存
- 这个写回内存的操作会使在其他CPU里缓存了该内存地址的数据无效
JMM级别volatile可见性
java内存模型
我们知道线程之间的通信机制有两种:共享内存和消息传递,java并发采用的是 共享内存 模型。
在java中所有的实例域、静态域和数组元素都存储在堆内存中,堆内存在线程之间共享。
因此共享内存问题主要指这部分的数据。java线程之间的通信由JMM控制,它决定了一个线程对共享变量的写入何时对另一个线程可见。
JMM抽象结构示意图如下。
JMM定义了线程和主内存之间的抽象关系:
- 线程之间的共享变量存储在主内存中
- 每个线程都有一个私有的本地内存,本地内存中存储了该线程以读写共享变量的副本。
本地内存是JMM定义的一个抽象概念,其实是涵盖了缓存、写缓冲区、寄存器以及其他的硬件和编译器优化
volatile内存语义
-
volatile写 的内存语义:
当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值刷新到主内存。 -
volatile读 的内存语义:
当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效。
volatile——有序性
重排序与Happens-before规则
在执行程序时,为了提高性能,编译器和处理器通常会对指令做 重排序 。
重排序分为三种类型:
重排序步骤 | 类型 | JMM对此排序的策略 |
---|---|---|
1. 编译器优化的重排序 | 编译器 | 禁止特定类型的编译器排序 |
2. 指令级并行的重排序 | 处理器 | 插入指定类型的内存屏障 |
3. 内存系统的重排序 | 处理器 | 插入指定类型的内存屏障 |
从JDK5开始,java使用新的JSR-133内存模型,它使用 happens-before 的概念来阐述两个操作之间的有序性。
这里的两个操作,既可以是指同一个线程内,也可以是多线程之间。
- 我们在此只关注其中的volatile变量规则:
对一个volatile域的写,happends-before于任意后续对这个volatile域的读。
此规则的实现也分为了两个部分:JMM针对于编译器制定的volatile重排序规则、基于保守策略的JMM内存屏障插入策略。
happens-before规则定义
JMM通过happens-before关系向程序员提供跨线程的内存可见性及有序性保证。
如果A线程的写操作a与B线程的读操作b之间存在着happens-before关系,尽管a和b在不同的线程中执行,但JMM向程序员保证:
- a写操作将对b读操作可见。
- a写操作的执行顺序将排在b读操作之前
- 并不意味着java平台的具体实现要按照happens-before关系指定的顺序来执行,只要重排序之后的执行结果是一致的,这种重排序是允许的。
与as-if-serial语义类似:保证单线程内程序的执行结果不被改变,happens-before保证了多线程的结果不被改变
happens-before规则意义
一个happens-before规则对应了多个编译器和处理器的重排序规则,对于程序员的意义在于,避免了我们去学习具体的重排序规则与可见性的保证,取而代之只需要理解其提供的几个简单易懂的规则。
附.volatile应用——DCL双重检查锁单例模式
我们从头开始实现几种单例模式,循序渐进分析volatile在DCL中所发挥的作用。
1.饿汉模式
public class Singleton{
private static final Singleton instance = new Singleton();
private Singleton(){}
public static Singleton getInstance(){
return instance;
}
}
这种方式的单例对象在类加载时就被初始化,当然不存在线程安全的问题。
但如果我们希望在用到此实例的时候再进行对象的创建和初始化呢?
2. 懒汉模式
public class Singleton {
private static Singleton instance;
private Singleton (){}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
这种方式解决了上述问题,但是也触发了线程安全问题:
如果线程A和线程B同时请求了getInstance方法,那么它们将都进行new Singleton()对象的创建,不能保证单例模式。
3. 线程安全懒汉模式
public class Singleton {
private static Singleton instance;
private Singleton (){}
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
与第2种方式的区别就在于,getInstance方法之前加上了synchronized关键字。
这种方式的问题:如果有大量线程同时请求,那么将串行地执行getInstance方法,进行大量不必要的等待。
4. 双重锁校验懒汉模式
public class Singleton {
private static Singleton instance;
private Singleton (){}
public static Singleton getSingleton() {
if (instance == null) { //1
synchronized (Singleton.class) {//2
if (instance == null) {//3
instance = new Singleton();
}
}
}
return instance ;
}
}
与第3种方式的区别为:synchronized关键字并非加到了整个方法上,而是方法内。
- 当instance不为空时,线程直接跳出了代码1的判断返回实例;
- 当instance为空时,假如线程A和线程B同时通过了代码1的判断,将串行地执行代码2中的片段;
- 假如线程A先进入同步区,通过new Singleton()创建实例;
- 此时线程B再进入,通过代码3的double check,保证了不会重复地创建实例,而是直接返回已有实例;
但是,这种方式也并不是完美的,如果我们考虑JVM的指令重排序的情况的话。
instance = new Singleton()这行代码,其实在JVM中并不是一个原子性的操作,而是有3个步骤:
- 给instance分配内存空间
- new Singleton()初始话实例
- 将创建的实例对象指向分配的内存空间;
当线程A进入在执行这个操作时,如果指令被重排序为1-3-2,那么在执行完3时instance就不再是null
线程B此时如果正在进行代码1中(instance == null)的判断,将会直接返回一个未初始化完成的instance,导致错误。
5. 双重锁校验懒汉模式+volatile关键字
public class Singleton {
private static volatile Singleton instance;
private Singleton (){}
public static Singleton getSingleton() {
if (instance == null) { //1
synchronized (Singleton.class) {//2
if (instance == null) {//3
instance = new Singleton();
}
}
}
return instance ;
}
}
与方式4唯一的区别就是给instance变量增加了一个volatile关键字修饰。
针对这种情况,我们不必关注它是如何进行指令的排序的,JMM为我们提供了一个happens-before原则:对一个volatile域的写,happens-before于任意后续对这个volatile域的读。
所以这种方式中,instance被volatile修饰,那么线程A对于instance赋值,一定是先于线程B对于instance的读取操作。