- volatile关键字的作用是什么?
- volatile能保证原子性吗?
- 之前32位机器上共享的long和double变量的为什么要用volatile? 现在64位机器上是否也要设置呢?
- i++为什么不能保证原子性?
- volatile是如何实现可见性的? lock前缀。
- volatile是如何实现有序性的? happens-before等
- 说下volatile的应用场景?
Volatile的读写语义
当一个线程修改了Volatile 变量 并且写回主内存,其他线程本地内存的这个变量的值会全部失效,重新从主内存读取。Volatile的作用
禁止指令重排
用DCL(双重检锁)说明,Volatile的禁止指令重排。 private static volatile User user;
public static User DCL() {
if (user == null) {
synchronized (Main.class) {
if (user == null) {
user = new User();
}
}
}
return user;
}
为什么要对user 对象使用了volatile关键字。
正常对象创建的过程:
- 分配内存空间
- 初始化对象
- 返回引用
如果发生指令重排,那么顺序可能变为 1.分配内存空间, 2. 返回引用, 3. 初始化对象。
所以为了防止返回一个未初始化对象,造成难以预料的问题,使用Volatile 关键字 禁止指令重排。
保证内存可见
```java public static boolean flag = true; public static void testMemoryVisible(){ Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
while (flag) {
}
System.out.println("我退出了");
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
flag = false;
System.out.println("改变flag 为" + flag);
}
});
t1.start();
try{
//延迟2s 后执行flag = false
Thread.sleep(2000);
}catch(Exception e){
e.printStackTrace();
}
t2.start();
}

程序仍在运行。因为t1将 flag 读取到本地内存以后,while使用的flag 仍然是本地内存的值。没有从住内存中重新读取flag的新值。

使用Volatile关键字后 保证内存可见,flag值更新后,while需要从主内存重新读取flag的值为false,退出循环。
<h2 id="N1cLD">Volatile的原理</h2>
<h3 id="L8Jjm">可见性原理</h3>
:::info
Volatile的可见性是通过lock前缀执行实现的。
:::
```java
public class Test {
private volatile int a;
public void update() {
a = 1;
}
public static void main(String[] args) {
Test test = new Test();
test.update();
}
}
以上代码转换成汇编指令。
0x0000000002951563: and $0xffffffffffffff87,%rdi
0x0000000002951567: je 0x00000000029515f8
0x000000000295156d: test $0x7,%rdi
0x0000000002951574: jne 0x00000000029515bd
0x0000000002951576: test $0x300,%rdi
0x000000000295157d: jne 0x000000000295159c
0x000000000295157f: and $0x37f,%rax
0x0000000002951586: mov %rax,%rdi
0x0000000002951589: or %r15,%rdi
0x000000000295158c: lock cmpxchg %rdi,(%rdx) //在 volatile 修饰的共享变量进行写操作的时候会多出 lock 前缀的指令
0x0000000002951591: jne 0x0000000002951a15
0x0000000002951597: jmpq 0x00000000029515f8
0x000000000295159c: mov 0x8(%rdx),%edi
0x000000000295159f: shl $0x3,%rdi
0x00000000029515a3: mov 0xa8(%rdi),%rdi
0x00000000029515aa: or %r15,%rdi
可以看见在11行 有个lock前缀。
在多核处理器中lock前缀有3个作用:
- 将当前缓存行数据写入主内存
- 数据写入主内存后,使其他线程共享变量副本所在缓存行失效。
- 有类似于内存屏障的作用,禁止该指令与前面和后面的指令进行重排序。
线程共享副本失效后 会从主内存重新读取,达到变量修改后就可见的效果。
volatile底层实现就是通过lock前缀执行,使用总线嗅探和MESI协议来实现内存可见性的。
总线锁定和缓存锁定
总线锁定 :处理器提供`LOCK#`信号,锁住整个总线,其他CPU核心请求会被阻塞,让当前核心独享整个内存。缓存锁定:基于缓存一致性协议,进行缓存的更新或者失效。(如果数据所在内存跨缓存行,那么会降级为总线锁定)
总线窥探
是一种保持缓存一致性的一种方案(基于缓存锁定),当数据被多个缓存共享时,处理器修改了共享数据的值后,必须传播到其他具有该数据副本的缓存中。事件传播可以通过总线窥探来达到,每一个缓存行都是一个窥探者,当总线传播出有数据更改的行为,每一个窥探者会检测是否持有该数据副本,如果持有该数据副本,会根据 **缓存一致性协议**做出不同的抉择(缓存失效或者缓存更新)。
缓存一致性协议。
如MSI、MESI(又名Illinois)、MOSI、MOESI、MERSI、MESIF、write-once、Synapse、Berkeley、Firefly和Dragon协议等等。MESI协议
MESI协议是一个基于写失效(监测到数据更改后,缓存行失效)的缓存一致性协议。允许缓存到缓存之间的数据副本复制。禁止重排序原理
禁止重排序是通过**内存屏障实现的。**volatile 修饰的变量户通过增加内存屏障实现静止重排序(内存屏障:静止屏障两边指令进行重排序),在JVM中提供了四种内存屏障:
1. LoadLoad屏障:举例语句是Load1; LoadLoad; Load2 (这句里面的LoadLoad里面的第一个Load对应Load1加载代码,然后LoadLoad里面的第二个Load对应Load2加载代码),此时的意思就是,在Load2及后续读取操作从内存读取数据到CPU前,保证Load1从主内存里要读取的数据读取完毕。
2. StoreStore屏障:举例语句是 Store1; StoreStore; Store2 (这句里面的StoreStore里面的第一个Store对应Store1存储代码,然后StoreStore里面的第二个Store对应Store2存储代码)。此时的意思就是在Store2及后续写入操作执行前,保证Store1的写入操作已经把数据写入到主内存里面,确认Store1的写入操作对其它处理器可见。
3. LoadStore屏障:举例语句是 Load1; LoadStore; Store2 (这句里面的LoadStore里面的Load对应Load1加载代码,然后LoadStore里面的Store对应Store2存储代码),此时的意思就是在Store2及后续代码写入操作执行前,保证Load1从主内存里要读取的数据读取完毕。
4. StoreLoad屏障:举例语句是 Store1; StoreLoad; Load2 (这句里面的StoreLoad里面的Store对应Store1存储代码,然后StoreLoad里面的Load对应Load2加载代码),在Load2及后续读取操作从内存读取数据到CPU前,保证Store1的写入操作已经把数据写入到主内存里,确认Store1的写入操作对其它处理器可见。