文章目录
- 一、volatile关键字特性
- 1、概念
- 2、特性
- 可见性
- 有序性(禁止指令重排序)
- 原子性
- 二、使用场景
- 模式1:状态标志
- 模式2:独立观察(independent observation)
- 模式3:一次性安全发布
- 模式4:“volatile bean” 模式
- 模式5:开销较低的“读-写锁”策略
- 参考资料
一、volatile关键字特性
Java并发编程包含三个基本概念
- 原子性:一(多)个操作要么全部执行要么不执行,中途不会被打断;
- 可见性:一个线程对某变量的修改对其他线程来说是可见的,即能知道值进行过修改;
- 有序性:程序执行按照代码的顺序执行;
1、概念
volatile
关键字是JVM提供的轻量式同步锁机制,而另一个常用的synchronized
为重量式同步锁机制。volatile
的轻量体现在它不会引起线程上下文的切换和调度,但是volatile
变量的同步性较差(但有时它更简单并且开销更低),而且其使用也更容易出错。
2、特性
需要注意的是,
volatile
只保证了并发编程三个概念中的有序性和可见性,并不能保证原子性(线程安全)。
可见性
Java内存模型如上,需要注意以下几点:
- 所有线程的共享变量都保存在主内存中
- 每个线程都有自己的工作内存,其中保存主内存中共享变量的副本
- 线程对共享变量进行修改时,先会对工作内存中的值进行修改,之后在传入主内存中
显然,这种值更新机制在多线程的场景下会产生很大的问题,典型的例子就是两个线程同时对同一共享变量进行修改,由于上面的步骤,会导致主内存中的值只是后提交线程的修改结果,而前一进程的修改遗失。此时需要采用同步机制来消除这一问题,当然可以使用synchronized
或用同步锁(Lock)来解决这一问题,但这些方式增加了较大的系统开销,因此不建议使用;而通过volatile
关键字修饰该共享变量可以很好的解决这一问题,因为一旦某线程对该共享变量的值进行修改,那么会立即刷新会主内存中,并且其他线程工作内存中的变量副本也失效,需要重新到主内存中读取共享变量的值。这里可以看成是一种通知行为,一旦用volatile
修饰的变量发生改变,系统会通知其他线程该变量已经改变(将其他线程中该变量设置为无效状态),需要重新读取。由此volatile
保证了可见性。
有序性(禁止指令重排序)
重排序是指编译器和处理器为了优化程序性能而对指令序列进行排序的一种手段。重排序需要遵守一定规则:
- 重排序操作不会对存在数据依赖关系的操作进行重排序
- 重排序不能改变程序执行结果
指令重排序的缺陷:(例:双重检查加锁)
public class Singleton {
private volatile static Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) { //1
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();//关键代码
}
}
}
return instance;
}
}
上述场景中,关键代码非原子操作,会分解为如下指令:
- 为instance分配内存
- 初始化instance
- 将instance变量指向分配的内存空间
显然第二三条操作间无依赖关系,因此JVM可能会因为优化而造成132的指令执行序列。但如果一个线程根据132的顺序执行时,在第三条指令执行完后,此时其他线程可能会因为1处的判断返回一个未初始化完全的实例,造成错误。
因此volatile
通过禁止指令重排序避免了这一现象的发生。
volatile
禁止指令重排序的要求是执行到volatile变量时,其前面的所有语句都执行完,后面所有语句都未执行。且前面语句的结果对volatile
变量及其后面语句可见。
原子性
其实从前面的分析中可以看出,volatile
保证了修改共享变量的可见性,也避免了因指令重排序而造成的同步错误。但是volatile却对正常执行序下的指令对共享变量的修改未加限制,因此会导致一个关键问题的发生——线程安全。
假设volatile
修饰的共享变量正在执行一个前后值存在依赖关系的操作,例如num++,此时分为三个步骤:
获取num的值
num的值加1
将num写回主内存
在这种情况下,一旦在执行第一条指令执行时存在其他线程对该共享变量的值进行过修改,那么当前线程也无法知晓(可见性只保证了第一条指令获取时得到的是最新修改的变量值),因此导致后面的指令会屏蔽之前的修改,导致错误。
所以volatile
无法保证线程安全,如果要解决上面代码的多线程安全问题,可以采取加锁synchronized
的方式,也可以使用JUC包下的原子类AtomicInteger
。
《java编程思想》一书上(p681)明确表明使用
volatile
而不是synchronized
的唯一安全情况是类中只有一个可变的域。如果一个域的值依赖于它之前的值时(类似上面的计数器),或是一个域的值受到其它域的限制,那么volatile
就无法工作。
二、使用场景
只能在有限的一些情形下使用 volatile
变量替代锁。要使 volatile
变量提供理想的线程安全,必须同时满足下面两个条件:
- 对变量的写操作不依赖于当前值
- 该变量没有包含在具有其他变量的不变式中
模式1:状态标志
这种情况下,
volatile
用来指定具有一个状态转换的标志变量。
模式2:独立观察(independent observation)
定期 “发布” 观察结果供程序内部使用。
模式3:一次性安全发布
某个对象引用的更新值(由另一个线程写入)和该对象状态的旧值同时存在。(双重检查加锁问题)
模式4:“volatile bean” 模式
volatile bean
模式的基本原理是:很多框架为易变数据的持有者(例如HttpSession
)提供了容器,但是放入这些容器中的对象必须是线程安全的。在volatile bean
模式中,JavaBean 的所有数据成员都是 volatile 类型的,并且getter
和setter
方法必须不包含约束。
模式5:开销较低的“读-写锁”策略
如果读操作远远超过写操作,可以结合使用内部锁和 volatile 变量来减少公共代码路径的开销。