前言
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并发编程:volatile关键字解析 #### 一、内存模型的相关概念 在深入了解`volatile`关键字之前,我们首先需要理解计算机内存模型的一些基本概念。在现代计算机系统中,CPU为了提高执行效率,会将频繁访问的...
Java面试官最爱问的volatile关键字是Java并发编程中一个重要的概念,了解volatile关键字可以帮助开发者更好地理解Java内存模型(JMM)和Java并发编程的特性。本文将详细介绍volatile关键字的方方面面,包括其作用、...
在深入理解Java内存模型之前,我们需要先了解并发编程模型的分类,然后掌握Java内存模型的基础知识,理解重排序和顺序一致性,以及volatile关键字的相关知识点。 首先,让我们探讨Java内存模型的基础知识。在并发...
Java内存模型(JMM),不同于Java运行时数据区,JMM的主要目标是定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中读取数据这样的底层细节。JMM规定了所有的变量都存储在主内存中,但每个...
Java内存模型,简称JMM(Java Memory Model),是Java虚拟机规范中定义的一个抽象概念,它描述了在多线程环境下,如何保证各个线程对共享数据的一致性视图。JMM的主要目标是定义程序中各个变量的访问规则,以及在...
Java内存模型(JMM)规定了线程如何访问和修改共享变量,`volatile`关键字正是在此模型下发挥作用。它通过内存屏障(内存栅栏)来防止指令重排序,并强制将更新后的值立即写回主内存,确保所有线程都能看到最新的值...
在深入理解volatile的关键特性之前,我们需要先了解Java内存模型(JMM,Java Memory Model)的基本概念。 JMM规定,每个线程都有自己的工作内存,用于存储从主内存中复制的共享变量副本。线程执行运算时是基于工作...
Java并发教程之volatile关键字详解 Java并发教程之volatile关键字的相关资料,对大家学习或者使用Java具有一定的参考学习价值。在Java中,volatile关键字是解决多线程问题的重要工具。本文将会详细介绍volatile...
Java内存模型,简称JMM(Java Memory Model),是Java编程语言规范的一部分,它定义了程序中各个线程如何访问和修改共享变量,以及如何确保数据的一致性。深入理解Java内存模型对于编写高效的并发程序至关重要。本文...
这个关键字对于理解Java内存模型(JMM)以及如何编写线程安全的代码至关重要。下面我们将从多个角度深入解读Java中的`volatile`关键字。 1. **可见性**:`volatile`关键字确保了变量的修改对所有线程是立即可见的。...
Java虚拟机有自己的内存模型(Java Memory Model,JMM),JMM可以屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的内存访问效果。JMM定义了线程和主内存之间的抽象关系:共享...
Java内存模型(JMM,Java Memory Model)是Java平台中用于描述如何在多线程环境中管理内存的一套规范。它确保了并发编程时不同线程之间的数据一致性、可见性和原子性,以避免出现数据竞争和其他并发问题。以下是JMM...
### 三、Java内存模型(JMM)与共享变量的可见性 Java内存模型规定了线程如何与主内存交互,每个线程都有自己的工作内存,其中保存了从主内存中读取的变量副本。`volatile`关键字的作用在于强制线程每次使用变量时...
Java内存模型(Java Memory Model, JMM)是Java平台中用于规范线程间通信和内存可见性的重要概念,它的目标是确保多线程环境下的正确同步。然而,原始的JMM存在一些严重的缺陷,导致了开发者在理解和实现线程安全时...
Java内存模型(Java Memory Model,简称JMM)是Java虚拟机(JVM)规范中定义的一种内存模型,它涉及了线程之间共享变量的可见性问题。在并发编程中,理解Java内存模型对于编写正确的多线程程序至关重要。 首先,...
Java内存模型通过synchronized和volatile关键字来保证可见性。 4. 原子性:一个操作要么全部完成,要么不完成,不会被其他线程打断。Java内存模型通过synchronized和volatile以及CAS(Compare and Swap)操作来保证...
Java内存模型(Java Memory Model,JMM)是Java平台中非常关键的概念,它定义了线程如何共享和访问内存中的数据,以及在多线程环境下如何保证数据的一致性。这本书"深入理解Java内存模型"显然是为了帮助读者深入探讨...
Java内存模型,简称JMM(Java Memory Model),是Java编程语言规范的一部分,它定义了线程如何共享和访问内存,以及在多线程环境中如何保证数据一致性。理解JMM对于编写高效、正确且线程安全的Java代码至关重要。 ...
Java内存模型通过使用volatile关键字来解决这个问题。volatile确保了对变量的修改对其他线程立即可见,禁止了缓存中的值被长期保留。 2. 原子性问题:Java内存模型并不保证所有的操作都是原子性的。例如,`count +=...