Java并发编程系列:详解volatile关键字

在整个并发知识体系(不断扩充)下,本文主要讨论的内容:
在这里插入图片描述

一、简介

volatile可以说是java虚拟机提供的最轻量级的同步机制,因为它不会引起线程上下文的调度和切换,执行成本比synchronized低。Java内存模型(JMM)告诉我们,各个线程会将共享变量从主内存中拷贝到工作内存,然后执行引擎会基于工作内存中的数据进行操作处理。线程在工作内存进行操作后何时会写到主内存中?这个时机对普通变量是没有规定的,而针对volatile修饰的变量给java虚拟机特殊的约定,线程对volatile变量的修改会立刻被其他线程所感知,即不会出现数据脏读的现象,从而保证数据的“可见性”volatile适用于一个写多个读的情况

二、volatile实现原理

  • Lock前缀指令
  • MESI协议

在此处要引入一个硬件知识,就是CPU Cache(高速缓存),计算机的存储层次为寄存器、cpu cache、内存、磁盘。cpu不会直接和内存进行交互,他们之间有一个cpu cache作为缓存,提高系统的运行速度。
一个volatile变量,在生成汇编代码时会在volatile修饰的共享变量进行写操作的时候会多出Lock前缀的指令

volatile实现原理:

  • 1、Lock指令会引起处理器将缓存写回到内存
    如果对声明了volatile的变量进行写操作,JVM就会向处理器发送一条Lock前缀的指令,将这个变量所在缓存行的数据写回到系统内存
  • 2、使用EMSI或嗅探机制保证缓存一致性
    但是写回到内存,如果其他处理器缓存的值还是旧的,再执行计算操作就会有问题。所以,在多处理器下,为了保证各个处理器的缓存是一致的,就需要缓存一致性协议:MESI、总线嗅探机制。不懂这两个名词的可以自行查阅,涉及到cpu cache缓存一致性知识,还可以拓展cache伪共享问题,cpu cache 存储结构,这里不再详细叙述

2.1 可见性

上面的讲解其实也就是保证了volatile的可见性

锁的happens-before锁规则,保证了释放锁和获取锁的两个线程间的内存可见性,也就是说对一个volatile变量的读,总能看到(任意线程)最后一次对这个变量的写。

happens-before规则有一个volatile变量规则:对一个volatile域的写,happens-before于任意后续对这个volatile域的读。这个规则保证了可见性和有序性。

那么从内存语义的角度来讲,volatile的写-读和锁的释放-获取具有相同的内存效果。

三、volatile写-读的内存语义

假设线程A先执行writer方法,线程B随后执行reader方法,初始时线程的本地内存中flag和a都是初始状态,下图是线程A执行volatile写后的状态图。

public class VolatileExample {
    private int a = 0;
    private volatile boolean flag = false;
    public void writer(){
        a = 1;          //1
        flag = true;   //2
    }
    public void reader(){
        if(flag){      //3
            int i = a; //4
        }
    }
}

线程A执行volatile写后的内存状态图

当volatile变量写后,线程中本地内存中共享变量就会置为失效的状态,因此线程B再需要读取从主内存中去读取该变量的最新值。下图就展示了线程B读取同一个volatile变量的内存变化示意图。

线程B读volatile后的内存状态图

从横向来看,线程A和线程B之间进行了一次通信,线程A在写volatile变量时,实际上就像是给B发送了一个消息告诉线程B你现在的值都是旧的了,然后线程B读这个volatile变量时就像是接收了线程A刚刚发送的消息。(EMSI协议)

3.1 volatile内存语义的实现

volatile内存语义的实现是由读写屏障来实现的。
内存屏障中的读写屏障——并发问题
一文解决内存屏障

JMM内存屏障分为四类见下图,

内存屏障分类表

java编译器会在生成指令系列时在适当的位置会插入内存屏障指令来禁止特定类型的处理器重排序。为了实现volatile的内存语义,JMM会限制特定类型的编译器和处理器重排序,JMM会针对编译器制定volatile重排序规则表

volatile重排序规则表

"NO"表示禁止重排序。为了实现volatile内存语义时,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。对于编译器来说,发现一个最优布置来最小化插入屏障的总数几乎是不可能的,为此,JMM采取了保守策略:

  1. 在每个volatile写操作的前面插入一个StoreStore屏障;
  2. 在每个volatile写操作的后面插入一个StoreLoad屏障;
  3. 在每个volatile读操作的后面插入一个LoadLoad屏障;
  4. 在每个volatile读操作的后面插入一个LoadStore屏障。

需要注意的是:volatile写是在前面和后面分别插入内存屏障,而volatile读操作是在后面插入两个内存屏障

StoreStore屏障:禁止上面的普通写和下面的volatile写重排序;

StoreLoad屏障:防止上面的volatile写与下面可能有的volatile读/写重排序

LoadLoad屏障:禁止下面所有的普通读操作和上面的volatile读重排序

LoadStore屏障:禁止下面所有的普通写操作和上面的volatile读重排序

下面以两个示意图进行理解,图片摘自相当好的一本书《java并发编程的艺术》。

volatile写插入内存屏障示意图

volatile读插入内存屏障示意图

四、可见性vs原子性

在这里插入图片描述

  • Synchronized可保证代码块的原子性,但是其性能相对较低

参考资料

  • 《Java并发编程的艺术》
  • 《深入理解Java虚拟机》
  • 黑马JUC
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

兴涛

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值