推荐阅读
- 学习笔记 《 深入理解 Java 虚拟机》
- 学习笔记 《 后端架构设计》
- 学习笔记 《 Java 基础知识进阶》
- 学习笔记 《 Nginx 学习笔记》
- 学习笔记 《 前端开发杂记》
- 学习笔记 《 设计模式学习笔记》
- 学习笔记 《 DevOps 最佳实践指南》
- 学习笔记 《 Netty 入门与实战》
- 学习笔记 《 高性能MYSQL》
- 学习笔记 《 JavaEE 常用框架》
- 学习笔记 《 Java 并发编程学习笔记》
- 学习笔记 《 分布式系统》
- 学习笔记 《 数据结构与算法》
volatile的作用我们都知道,他是在多线程之间实现可见性的保证,那么在JMM中
- volatile的内存语义以及其实现你真的了解吗?
- 为什么使用volatile就可以保证内存可见性?
- 为什么说volatile保证可见性却不能保证原子性?
在之前的 Java的内存模型 的Happen-Before 的几个原则中就说到 ,对于volatile变量的写操作先于发生任意后续这个变量的读操作 ,也就是说对volaitile的写操作可以对任意其他线程的读操作可见。那么本章节就着重了解一些volatile的内存语义与实现原理。在JSR-133 中
1、Volatile 特性
理解volatile特性的一个好方法是把对volatile变量的单个读/写,看成是使用同一个锁对这些单个读/写操作做了同步。下面通过具体的示例来说明,示例代码如下。
class Test{
volatile Long count;
public Long getCount() {return count; }
public void setCount(Long count) { this.count = count; }
public void increment(){ this.count++; }
}
假设多线程执行程序,那么其执行效果等效于
class Test{
private Long count;
public synchronized Long getCount() {
return count;
}
public synchronized void setCount(Long count) {
this.count = count;
}
public synchronized void increment(){
this.count++;
}
}
如上面示例程序所示,一个volatile变量的单个读/写操作,与对一个普通变量的的加锁读/写操作,它们之间的执行效果相同
锁的happens-before规则保证释放锁和获取锁的两个线程之间的内存可见性,这意味着对一个volatile变量的读,总是能看到(任意线程)对这个volatile变量最后的写入。
2、 volatile 写/读建立的 happens-before关系
从volatile 的内存语义上来说,volatile的写-读与所得释放-获取有相同的效果;volatile 写和锁的释放具有相同的内存语义,volatile读与所得获取有相同的内存语义。
比如下面的程序中
class VolatileExample {
int a = 0;
volatile boolean flag = true;
public void writer(){
a = 1; // 1
flag = true; // 2
}
public void reader(){
if(flag){ // 3
int i = a; // 4
}
}
}
线程A执行writer() 方法,之后线程B执行reader()方法, 根据As-Serial-if 规则可知 1 happens-before 2 , 3 happens-before 4 ,根据volatile写-读规则可知,2 hapens-beofre 3,所以1 happens-before 4 ,所以可知 根据volatile 规则可可以构建Happens-Before 规则。
3、Volatile 的内存语义与其实现原理
3.1 volatile 写操作与读操作的内存语义
-
当写一个Volatile 变量的时候,JMM会把该线程对应的本地内存的共享变量刷新到主内存中. ** 使用上面的代码中示例,当在执行writer() 方法 写了flag变量后,JMM会立即可本地内存的共享变量刷新到主内存中。
-
当读取一个volatile变量的时候,JMM会将当前线程的本地内存共享变量设置为无效,线程在接下来的操作,会从主内存中读取共享变量。
所以,volatile 的内存语义就是,在修改一个volatile变量的时候,会将本地内存的共享变量刷新到主内存,然后读取一个volatile变量的时候会将本地内存作废,然后从主内存中读取数据。所以线程1更新volatile变量的值,线程2读取volatile变量,实际上就是线程1向线程2发送消息通知线程2共享变量发生改变。
3.2 volatile 内存语义的实现
在之前的笔记中,笔者就已经提到过,JMM为了实现volatile内存语义使用的就是在指令序列中插入不同的内存屏障指令。JMM 有四种内存屏障指令,用于不同的场景,JMM的场景如下:
- 在每个volatile 写操作前面添加 StoreStore 屏障
- 在每个volatile 写操作后面插入 StoreLoad 屏障
- 在每个volatile 读操作后面插入 LoadLoad 屏障
- 在每个volatile 读操作后面插入 LoadStore 屏障
3.3 在volatile 写操作之前插入StoreStore 屏障
因为在对volatile 写操作之后需要同步刷新到主内存中,此时如果对volatile写操作之前的普通写操作重排序到volatile写操作之后,那么这个普通写的操作呢就不能被刷新到主内存中,所以为了防止出现这种不可见的情况,所以在volatile写操作之前需要插入StoreStore 屏障,防止之前的普通写操作重排序到volatile写操作之后。
3.4 在volatile 写操作之后插入StoreLoad 屏障
StoreLoad 防止volatile写操作后面的可能存在volatile读操作重排序,按照正常的未重排序的操作,首先会执行volatile写操作,然后同步到主内存,然后volatile读操作会从主内存中读取数据,如果出现重排序,可能会出现先执行volatile读操作,在出现volatile 写操作。为了防止这种情况的重排需要插入StoreLoad屏障执行。
因为JMM无法确定volatile写操作后面是否会出现volatile读操作,所以采取了保守策略: 在每个volatile写操作后面或者在每个volatile读操作前面提示添加StoreLoad指令,但是常见的模型是一个线程写volatile,多个线程读volatile, 那么为了性能上的考虑,所以在每个volatile写操作后面添加了StoreLoad指令。
3.5 在volatile 读操作之后添加 LoadLoad 和 LoadStore 屏障
在执行volatile 操作之后,会将本地内存作废,所以在volatile读操作之后的在普通读操作不能排到volatile读操作之前,所以这里需要添加LoadLoad屏蔽。同时如果volatile读操作与下面的普通写操作排序,那么那么有可能会将普通写的操作作废,所以为了禁止这种情况,也需要在volatile读操作之后添加LoadStore屏障。
事实上,在实际执行的时候,只要不修改volatile的内存语义,一些场合下,可以省略部分内存屏障的指令,以提高效率。同时不同的处理器对于内存屏蔽也有不同的松紧度,