简介
volatile 是一种同步机制,比 synchronized 或 Lock 相关类更轻量,因此使用 volatile 并不会发生上下文切换等开销很大的行为。
如果一个变量被修饰成 volatile,那么 JVM 就知道了这个变量可能会被并发修改。
因为其开销小,所以对应的功能也小,volatile 不能像 synchronized 一样提供原子保护。
实现原理
Java语言规范第3版中对volatile的定义如下:Java编程语言允许线 程访问共享变量,为了确保共享变量能被准确和一致地更新,线程应 该确保通过排他锁单独获得这个变量
。Java语言提供了volatile,在某些 情况下比锁要更加方便。如果一个字段被声明成volatile,Java线程内存模型确保所有线程看到这个变量的值是一致的。
volatile 是如何保证可见性的?
首先通过工具获取 JIT 编译器生成的汇编指令来查看对volatile进行写操作时,CPU会 做什么事情。
java 代码:
instance = new Singleton(); // instance是volatile变量
转变成汇编代码,如下:
0x01a3de1d: movb $0×0,0×1104800(%esi);0x01a3de24: lock addl $0×0,(%esp);
有volatile变量修饰的共享变量进行写操作的时候会多出第二行汇 编代码,通过查IA-32架构软件开发者手册可知,Lock
前缀的指令在多核处理器下会引发了两件事情 。
- 将当前处理器缓存行的数据写回到系统内存。
- 这个写回内存的操作会使在其他CPU里缓存了该内存地址的数 据无效。
通过之前学习的 JMM 内存模 可以得知,为了提高处理速度,处理器不直接和内存进行通信,而是先将系 统内存的数据读到内部缓存(L1,L2或其他)后再进行操作,但操作完不知道何时会写到内存。如果对声明了volatile的变量进行写操作, JVM就会向处理器发送一条 Lock 前缀的指令,将这个变量所在缓存行 的数据写回到系统内存。但是,就算写回到内存,如果其他处理器缓 存的值还是旧的,再执行计算操作就会有问题。所以,在多处理器 下,为了保证各个处理器的缓存是一致的,就会实现缓存一致性协 议,每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是 不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会 将当前处理器的缓存行设置成无效状态,当处理器对这个数据进行修 改操作的时候,会重新从系统内存中把数据读到处理器缓存里。
volatile 的两条实现原则
1)Lock前缀指令会引起处理器缓存回写到内存;
2)一个处理器的缓存回写到内存会导致其他处理器的缓存无效 ;
主要特性
可见性
代码实践
public class MyData {
// 这里去掉 volatile 后 main 线程就发阻塞
volatile int num = 0;
public void addNum() {
num = 60;
}
}
class VolatileDome{
public static void main(String[] args) {
MyData myData = new MyData();
new Thread(() -> {
System.out.println(Thread.currentThread().getName());
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
myData.addNum();
System.out.println(Thread.currentThread().getName() + myData.num);
}, \"AAA\").start();
while (myData.num == 0) {
}
System.out.println(Thread.currentThread().getName() + \"mission is over\");
}
}
不保证原子性
public class MyData {
volatile int num = 0;
public void addNumPlus() {
num++;
}
}
class VolatileDome{
public static void main(String[] args) {
MyData myData = new MyData();
for (int i = 0; i < 20; i++) {
new Thread(() -> {
for (int j = 0; j < 1000; j++) {
myData.addNumPlus();
}
}, String.valueOf(i)).start();
}
while (Thread.activeCount() > 2) {
Thread.yield();
}
// 最终打印的结果很大可能不是 20W。
System.out.println(myData.num);
}
}
addNumPlus 字节码解读
0 aload_0
1 dup
2 getfield #2 <com/lhn/demo1/jmm/MyData.num : I>
5 iconst_1
6 iadd
7 putfield #2 <com/lhn/demo1/jmm/MyData.num : I>
10 return
通过字节码可以看出 n++ 被拆成了三个指令:
- 执行 getfieid 拿到原始 n;
- 执行 iadd 进行加 1 操作;
- 执行 putfieid 把累加后的值写回;
因为 volatile 不能保证原子性,所以在执行 num++ 的时候可能会被其它线程给中断操作,导致写入的值覆盖掉前一个线程写的值,出现丢失写值的情况。
解决原子性问题
使用 synchronized(重量级操作,不推荐)
public synchronized void addNumPlus() {
num++;
}
使用 atomic (推荐使用)
AtomicInteger atomicInteger = new AtomicInteger();
public void addAtomic() {
atomicInteger.getAndIncrement();
}
禁止指令重排序
计算机在执行程序时,为了提高性能,编译器和处理器常常会对指令做重排
,一般分一下三种:
graph LR
A[源代码] --> B[编译器优化的重排] --> c[指令并行的重排] --> d[内存系统的重排] --> e[最终执行的指令]
单线程环境
下确保程序最终执行结果和代码顺序执行的结果一样,处理器在进行重排序时必须要考虑指令之间的数据依赖性;
多线程环境
中线程交替执行,由于编译器优化重排的存在,两个线程中使用的变量能否保证一致性是无法确定的,结果无法预测。
实现原理
volatile 实现禁止指令重排序优化,从而避免多线程环境下程序出现乱序执行的现象。
这个首先要了解一个概念,就是内存屏障(Memory Barrier)
又称内存栅栏,是一个 CPU 指令,他的作用有两个:
- 保证特定操作的执行顺序。
- 保证某些变量的内存可见性(利用该特性实现 volatile 的内存可见性)
由于编译器和处理器都能执行指令重排序优化,如果在指令间插入一条 Memory Barrier
则会告诉编译器和 CPU,不管什么指令都不能和这条 Memory Barrier
指令重排序。也就是说通过插入内存屏障禁止在内存屏障前后的指令执行重新排序。
对 volatile 变量进行写操作时,会在写操作后加入一条 store 屏障指令,将工作内存中的共享变量值刷新回到主内存。
对 volatile 变量进行读操作时,会在写操作后加入一条 load 屏障指令,从主内存中读取共享变量。
volatile 的应用场景
引用地址:https://www.cnblogs.com/krcys/p/9385360.html
状态变量
由于boolean
的赋值是原子性的,所以volatile
布尔变量作为多线程停止标志还简单有效的.
Copyclass Machine{
volatile boolean stopped = false;
void stop(){stopped = true;}
}
对象完整发布
这里要提到单例对象的双重检查锁,对象完整发布也依赖于happens before
原则,有兴趣可以自己去查阅,这个原则是比较啰嗦,可以简单理解为我满足happens before
,那么我之前的代码按顺序执行.
Copypublic class Singleton {
//单例对象
private static Singleton instance = null;
//私有化构造器,避免外部通过构造器构造对象
private Singleton(){}
//这是静态工厂方法,用来产生对象
public static Singleton getInstance(){
if(instance ==null){
//同步锁防止多次new对象
synchronized (Singleton.class){
//锁内非空判断也是为了防止创建多个对象
if(instance == null){
instance = new Singleton();
}
}
}
return instance;
}
}
这是一个会产生bug的双重检查锁代码,instance = new Singleton()
并不是一步完成的,他被分为这几步:
Copy1.分配对象空间;
2.初始化对象;
3.设置instance指向刚刚分配的地址。
下面图中,线程A红色先获得锁,B黄色后进入.
这种情况会出现bug,但是由于volatile
满足happens before
原则,所以会等对象实例化之后再对地址赋值,我们需要将private static Singleton instance = null;
改成private static volatile Singleton instance = null;
即可.
来源:https://www.cnblogs.com/lhnstart/p/16001744.html
本站部分图文来源于网络,如有侵权请联系删除。