前言
volatile关键字是JVM提供的最轻量级的同步机制,但是由于其不容易被正确地、完整的理解,以至于许多程序员都习惯不去使用它,而总是选择synchronized重量级的锁机制来进行同步,本文将弄清楚volatile关键字的真正语义。
当一个变量被定义成volatile之后,它将具备两种语义特性:可见性、有序性。
一、可见性
当一条线程修改了一个volatile变量的值,新值是立即对其他线程可知的。这是普通变量所不能保证的,通过前文的JMM内存模型可知,普通变量的值在线程之间由于各个线程有各自的工作内存的原因,并不能做到随时同步。
导致volatile不能被容易理解的地方也就在这里,虽然对一个volatile变量的读取,都能保证获取到任意线程对这个volatile变量最后的写入,但是对volatile变量的复合操作(例如volatile++, volatile= volatile * x)仍然不具有原子性,因为volatile++这种复合操作实际上包含三个操作:读取、加1、将加1的结果赋值回写。volatile关键字只能保证第一个操作“读取”的结果是正确的,但是在执行后面两个操作的时候,其他线程依然可以甚至已经改变了volatile变量的值,使的现在操作的volatile变量已经是过期的数据。Volatile只能保证对修饰的变量的单次读或者写操作是原子性的(包括long和double类型)。
因此在不符合以下两条规则的运算场景中,我们仍然要通过加锁来保证对volatile变量操作的原子性。
1. 运算结果并不依赖变量的当前值,或者能够确保只有单一的线程修改变量的值。
2. 变量不需要与其他状态变量共同参与不变约束。
如下这种场景就非常适合使用volatile变量来控制并发。当shutdown()方法被调用时,能保证所有线程中执行的doWork()方法都能立即停下来。
volatile boolean shutdownRequested;
public void shutdown(){
shutdownRequested = true;
}
public void doWork(){
while(!shutdownRequested){
//do stuff
}
}
总的来说,当一个变量被volatile修饰后,表示着线程本地内存无效,当一个线程修改共享变量后他会立即被更新到主内存中,当其他线程读取共享变量时,它会直接从主内存中读取。
二、有序性
volatile变量的有序性通过禁止指令重排序优化来保证(volatile屏蔽指令重排序的语义在JDK1.5中才被完全修复,此前的JDK中即使将变量声明为volatile,也仍然不能完全避免重排序所导致的问题)。通过前文的重排序内容我们知道,普通的变量仅仅会保证在方法执行过程中所有依赖该变量赋值结果的地方通过禁止重排序保证都能获取正确的结果,而如果变量的赋值操作不被后面的操作所依赖,由于在方法的执行过程中无法感知到变量值的改变,所以这时候是可以进行重排序的,也就是as-if-serial语义所描述的行为。通过如下代码示例可以说明为何指令重排序可能会干扰程序的并发执行:
Map configOptions;
// 此变量必须定义为volatile
volatile boolean initialized = false;
// 假设以下代码在线程A中执行
// 模拟读取配置信息,读取完成之后,设置initialized为true来通知其他线程配置可用
configOptions = readConfigOptions(fileName);
initialized = true;
// 假设以下代码在线程B中执行
// 等待initialized 为true,代表线程A已经初始化完配置信息
while(!initialized){
sleep();
}
// 使用线程A初始化好的配置信息
doSomethingUseConfig();
如果initialized变量没有被定义为volatile,就可能由于指令重排序的优化导致最后一句的“initialized = true”被提前执行,从而线程B中使用配置信息的代码就可能出现错误,而volatile关键字可以避免这样的情况
三、底层实现原理
由上可知:
当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值立即刷新到主内存中。
当读一个volatile变量时,JMM会把该线程对应的本地内存设置为无效,直接从主内存中读取共享变量。
通过JMM之二内存屏障章节我们也可以知道,内存屏障可以禁止指令重排序以及影响数据可见性,这不正是volatile关键字的两层语义吗?其实,JVM底层就是采用“内存屏障”来实现volatile的语义。更多内容可以参考
http://gee.cs.oswego.edu/dl/jmm/cookbook.html
JMM针对编译器制定了如下的volatile重排序限制策略:
是否能重排序 |
第二个操作 |
第一个操作 |
普通读/写 |
volatile读 |
volatile写 |
普通读/写 |
|
|
NO |
volatile读 |
NO |
NO |
NO |
volatile写 |
|
NO |
NO |
- 当第一个操作是volatile读时,不论第二个操作是什么,都不能重排序。
- 当第二个操作是volatile写时,不论第一个操作是什么,都不能重排序。
- 当第一个操作是volatile写,第二个操作是volatile读或写时,亦不能重排序。
为了实现以上策略,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序,下面是基于保守策略的JMM内存屏障插入策略(在实际中,只要不改变volatile写-读得内存语义,编译器可以根据具体情况优化,省略不必要的屏障):
- 在每一个volatile读操作后面插入一个LoadLoad屏障,用于禁止处理器把前面的volatile读和后面的普通读重排序。
- 在每一个volatile读操作后面插入一个LoadStore屏障,用于禁止处理器把前面的volatile读和后面的普通写重排序。
- 在每一个volatile写操作前面插入一个StoreStore屏障,用于保证在volatile写之前,把前面的所有普通写操作都刷新到主内存中。
- 在每一个volatile写操作后面插入一个StoreLoad屏障,用于将本次的volatile写刷新到主内存,并且禁止处理器把前面的volatile写和后面可能有的volatile读或写操作重排序。
四、happens-before\Volatile运用分析之单例模式DCL机制
DCL即双重检查加锁,下面是一个典型的在单例模式中使用DCL的例子:
public class LazySingleton {
private int someField;
private static LazySingleton instance;
private LazySingleton() {
this.someField = new Random().nextInt(200)+1; // (1)
}
public static LazySingleton getInstance() {
if (instance == null) { // (2)
synchronized(LazySingleton.class) { // (3)
if (instance == null) { // (4)
instance = new LazySingleton();// (5)
}
}
}
return instance; // (6)
}
public int getSomeField() {
return this.someField; // (7)
}
}
这里得到单一的instance实例是没有问题的,问题的关键在于Singleton.getInstance().getSomeField()有可能返回someField的默认值0。
分析:假设线程Ⅰ是初次调用getInstance()方法,紧接着线程Ⅱ也调用了getInstance()方法和getSomeField()方法,线程Ⅱ在执行getInstance()方法的语句(2)时,由于对instance的访问并没有处于同步块中,因此线程Ⅱ可能观察到也可能观察不到线程Ⅰ在语句(5)时对instance的写入,也就是说instance的值可能为空也可能为非空。
情况一:线程Ⅱ在(2) 观察到了线程Ⅰ对instance的写入。那么instance非空,线程Ⅱ将不会执行(3),即不会进入同步块,直接执行(6)返回instance,然后对这个instance调用getSomeField()方法即语句(7),该方法也是在没有任何同步情况被调用,因此整个线程Ⅱ的操作都是在没有同步的情况下调用 ,这时我们便无法通过happen-before的8条规则得出线程Ⅰ的操作和线程Ⅱ的操作之间存在任何有效的happen-before关系,那么线程Ⅰ的语句(1)和线程Ⅱ的语句(7)之间自然也不存在happen-before关系,这就意味着线程Ⅱ在执行语句(7)完全有可能观测不到线程Ⅰ在语句(1)处对someFiled写入的值,所以根据happen-before原则可知,这种DCL设计是存在问题的。
情况二:线程Ⅱ在(2) 没有观察到线程Ⅰ对instance的写入。那么instance为空,线程Ⅱ将会执行(3)和(4),接下来根据happen-before规则可以得出如下关系:
锁定规则: 线程Ⅰ语句(5) happen-before 线程Ⅱ语句(3) 因为语句(5)处有unlock操作
程序次序规则: 线程Ⅱ语句(3) happen-before 线程Ⅱ语句(4)
传递性: 线程Ⅰ语句(5) happen-before 线程Ⅱ语句(4)
所以线程Ⅱ在执行语句(4)时一定能够观察到线程Ⅰ在语句(5)时对Singleton的写入值,所以线程Ⅱ执行语句(4)时将发现instance不为空,直接执行(6)返回由线程Ⅰ初始化的instance。
但是语句5实际上不是一个原子操作,它包含三个操作:
①.给LazySingleton分配内存地址。
②.初始化LazySingleton构造方法(即语句1)。
③.将instance对象指向分配的内存空间(在这一步的时候instance变成非null).
虽然根据程序次序规则,这三个操作之间存在happen-before 原则,但是根据不影响单线程运行结果,重排序是允许的。这里②和③两个操作之间不存在数据依赖,所以②和③可能会进行重排序执行(只有在X86架构下不存在对写人的重排序)。
在这种情况下,线程Ⅱ可能会拿到一个还未初始化完成的instance实例,所以依然可能无法在操作(7) 时获取到正确的值(当然在X86架构下应该不会出问题)。
对DCL的分析也告诉我们一条经验原则:对引用(包括对象引用和数组引用)的非同步访问,即使得到该引用的最新值,却并不能保证也能得到其成员变量(对数组而言就是每个数组元素)的最新值.
解决方案:
1. 利用“JSL保证的------一个类直到被使用时才被初始化,而类初始化的过程是非并行的”的思想借助静态内部类,这也是最简单而且安全的解决方法:
public class Singleton {
private Singleton() {}
// Lazy initialization holder class idiom for static fields
private static class InstanceHolder {
private static final Singleton instance = new Singleton();
}
public static Singleton getSingleton() {
return InstanceHolder.instance;
}
}
2. 利用volatile关键字,将成员变量instance声明为volatile:
private volatile static LazySingleton instance;
然后再次分析其happen-before关系:
volatile变量规则:线程Ⅰ语句(1) happen-before 线程Ⅰ语句(5),线程Ⅰ语句(5) happen-before 线程Ⅱ语句(2)
程序次序规则: 线程Ⅱ语句(2) happen-before 线程Ⅱ语句(6),线程Ⅱ语句(6) happen-before 线程Ⅱ语句(7)
传递性: 线程Ⅰ语句(1) happen-before 线程Ⅱ语句(7)
进一步的解释是,当instance被volatile修饰之后,操作1将不会被重排序到操作5中的将instance对象指向分配的内存空间之后,并且操作5完成之后还会立即对someField字段的写入操作以及对instance的赋值写入操作刷新到主存。在线程Ⅱ执行操作2或者4的时候,将会立即重新从主存加载instance对象以及其成员someField字段,所以当线程Ⅱ能够看到instance不为空时,也必定能够拿到someField字段最新的值。
这表示线程Ⅱ在语句(7)能够观察到线程Ⅰ在语句(1)时对someFiled的写入值,程序能够得到正确的行为。
分享到:
相关推荐
java内存模型jmm
Java内存模型(JMM),不同于Java运行时数据区,JMM的主要目标是定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中读取数据这样的底层细节。JMM规定了所有的变量都存储在主内存中,但每个...
JMM的目的之一是为了提供一个跨平台的内存模型,使得Java开发者可以更好地理解Java语言的语言特性和内存相关的内容。JMM的概念包括堆、栈、本机内存、防止内存泄漏等方面。 JSR133是JMM的一部分,它是Java语言规范...
深入Java内存模型-JMM。。。。。。。。。。。。。。。。。。
深入理解 Java 内存模型,由程晓明编著,深入理解java内存模型JMM
Java内存模型及Volatile底层实现原理
由于JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存(有些地方称为栈空间),工作内存是每个线程的私有数据区域,而Java内存模型中规定
JMM告诉我们,各个线程会将共享变量从主内存中拷贝到工作内存,然后执行引擎会基于工作内存中的数据进行操作处理。线程在工作内存进行操作后何时会写入主内存中?这个实际对普通变量没有规定的,而针对volatile修饰...
Java内存模型的抽象 重排序 处理器重排序与内存屏障指令 happens-before 重排序 数据依赖性 as-if-serial 语义 程序顺序规则 重排序对多线程的影响 顺序一致性 数据竞争与顺序一致性保证 顺序一致性内存模型 同步...
Java 内存模型的抽象 4 重排序 6 处理器重排序与内存屏障指令 7 happens-before 10 重排序 13 数据依赖性 13 as-if-serial 语义 13 程序顺序规则 15 重排序对多线程的影响 15 顺序一致性 19 数据竞争与顺序...
局部变量(Local variables),方法定义参数(java语言规范称之为formal method parameters)和异常处理器参数(exception handler parameters)不会在线程之间共享,它们不会有内存可见性问题,也不受内存模型的...
主要介绍了Java内存模型JMM详解,涉及volatile和监视器锁,final字段,内存屏障等相关内容,具有一定参考价值,需要的朋友可以了解下。
关于同步和线程安全的许多底层混淆是Java内存模型的一些难以直觉到的细微差别。本文还介绍了JMM有一些严重的缺点,如果正确地编写并发的类太困难的话,那么许多并发的类不能按预期工作,并且这是平台中的一个缺点。...
Agenda: •什么是Java内存模型JMM •内存可见性 •有序性 •指令重排序 •内存屏障 •顺序一致性与Happens-before规则 •volatile, synchronized, 原子变量,锁, final的原理
Java Memory Model简称JMM, 是一系列的Java虚拟机平台对开发者提供的多线程环境下的内存可见性、是否可以重排序等问题的无关具体平台的统一的保证。(可能在术语上与Java运行时内存分布有歧义,后者指堆、方法区、...
Java内存模型,即:JMM。当程序执⾏并⾏操作时,如果对数据的访问和操作不加以控制,那么必 然会对程序的正确性造成破坏。因此,我们需要在深⼊了解并⾏机制的前提下,再定义⼀种规则, 来保证多个线程间可以有效地...
主要介绍了学习Java内存模型JMM的心得以及对其原理做了深入的介绍,有兴趣的朋友学习下吧。
深入理解Java内存模型