简介
【volatile|volatile 关键字】volatile 是一种同步机制,比 synchronized 或 Lock 相关类更轻量,因此使用 volatile 并不会发生上下文切换等开销很大的行为。实现原理 Java语言规范第3版中对volatile的定义如下:
如果一个变量被修饰成 volatile,那么 JVM 就知道了这个变量可能会被并发修改。
因为其开销小,所以对应的功能也小,volatile 不能像 synchronized 一样提供原子保护。
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里缓存了该内存地址的数 据无效。
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 = https://www.it610.com/article/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 = https://www.it610.com/article/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
5 iconst_1
6 iadd
7 putfield #2
10 return
通过字节码可以看出 n++ 被拆成了三个指令:
- 执行 getfieid 拿到原始 n;
- 执行 iadd 进行加 1 操作;
- 执行 putfieid 把累加后的值写回;
解决原子性问题 使用 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;
即可.