volatile关键字原理

volatile的主要作用是在多核处理器开发中保证共享变量对于多线程的可见性

并发编程的线程安全,本质就是原子性,有序性,可见性

从硬件上来看,多核cpu的架构采用分级高速缓存
在这里插入图片描述
cpu缓存分为三层,level1有两个,一个是指令缓存,一个是数据缓存;level3为同一cpu中的多核共享
在这里插入图片描述
在多cpu的情况下,如果不同cpu之间需要共享数据则需要从主内存中获取

缓存锁

多个线程操作同一个被缓存的共享数据的原子性需要被被保证,所以当某个cpu需要操作一个被多线程缓存的共享数据时,只需要给这个被共享的数据进行加锁即可。除了加锁,还需要通过缓存的一致性机制来保证操作的原子性。

缓存的一致性就是多个cpu中缓存的同一个共享数据的数据一致性,MESI是使用比较广泛的缓存一致性协议,MESI是表示缓存的四种状态

M(modify)表示共享数据只缓存在当前cpu缓存中,并且是被修改的状态,也就是缓存的数据和主内存中的数据并不一致
E(exclusive)表示缓存的为独占状态,数据只缓存在当前的cpu中,并且没有被修改
S(shared)表示数据可能被多个cpu缓存,并且各个缓存中的数据和主内存中的数据一致
I(invalid)表示缓存已经失效

每个cpu不仅仅知道自己的读写操作,也会监听其他cache的读写操作,cpu读取缓存会遵守以下几个逻辑:
1.如果缓存的状态时I,那么就从内存中读取,否则直接从从缓存读取
2.如果缓存处于M或者E的cpu嗅探到其他cpu有读的操作,就把自己的缓存写入到内存,并把自己的状态设置为s
3.只有缓存状态时M或者E的时候,cpu才可以修改缓存中的数据,修改后,缓存的状态变为M

*例如:有A,B两个线程都对数据X进行操作,即A,B线程都从主内存中将X缓存到自己的cpu核心的缓存中

A先获取到数据X,B后获取到数据X

在A线程中,X的缓存状态变化:E(此时B还未获取到X) -->S(此时B已经获取到X) --> M(A修改了其cpu核心缓存中X对象,其他线程中的中的X缓存状态修改为I) -->回写进主内存

在B线程中,X的缓存状态变化:S(此时A率先获取了X进行缓存) -->I(此时A修改了其缓存的X对象,此时B需要从新从主内存中获取X的新值) -->等待A将X的新增回写进主内存 --> S或E?*

MESI缓存协议存在一个问题,就是当一个cpu修改当前缓存的共享数据时,会发送消息给其他缓存那个数据的cpu,在这个发送消息和其他cpu各自完成缓存状态切换的过程中,当前cpu会等待所有响应完成,这样会降低当前cpu的使用效率,为了解决这个问题,引入了StoreBuffers

当前cpu会先把要写入主内存中的值写入到StoreBuffers中,然后继续去处理其他指令,当所有其他cpu确认完成缓存状态切换后,StoreBuffers中的数据才最终被提交,但是在StoreBuffers等待提交的过程中,会出现其他指令依赖这个数据时,其所读取到的值不是最新的或者说是错误的,即产生了可见性的问题

内存屏障

内存屏障是将storebufferes中的指令写入到内存,从而使得访问同一共享内存的线程可见
X86的内存屏障指令包括Ifence(读屏障) sfence(写屏障) mfence(全屏障)

Store Memory Barrier,简称sfence(写屏障) 通知处理器在写屏障之前所有的存储在storebuffers中的数据同步到主内存中,即使得写屏障之前的指令的结果对写屏障之后的读或写是可见的,即相对于发起一个同步操作将storebuffers中的缓存同步到主内存中( 在写指令之后插入一个写屏障,让写入到storebuffers中的最新数据回写到主内存中)

Load memory barrier,简称Lfence(读屏障) 处理器在读屏障之后的读操作,都在读屏障之后执行,配合读写屏障,使得写屏障之前的内存更新对于读屏障之后的操作都是可见的(在读指令之前插入读屏障,可以让各缓存中的该变量数据失效,重新从主内存中加载数据,以保障缓存中的变量值为最新的)

Full memory barrier,简称mfence(全屏障) 确保屏障前的内存读写操作的结果提交到内存之后,再执行屏障后的读写操作

从JMM层面解决线程并发问题
硬件层面解决线程并发,原子性主要是通过缓存锁实现,有序性和可见性主要通过内存屏障实现。在软件层面要解决原子性,有序性,可见性问题主要通过JMM(java memory model)JAVA内存模型进行实现的

由于JAVA是属于跨平台语言,这意味着需要在JVM层面屏蔽各底层的差异,因此在JVM中定义了JMM
在这里插入图片描述
在这里插入图片描述
JAVA内存模型定义了线程和主内存的交互方式,在JVM抽象模型中分为主内存,工作内存;主内存是所有线程共享的(对象,静态变量,数组等存储在堆中的数据),工作内存是每个线程独占的(方法逻辑,局部变量的栈中的数据),线程对变量的所有操作都必须在工作内存进行,线程的工作内存保存了从主内存中拷贝而来的变量副本,不能直接读写主内存中的变量,本线程的工作内存也无法被其他线程直接访问,线程之间的共享变量值的传递都是基于主内存来完成
在JMM中定义了8个原子操作:lock,unlock,read,load,use,assign,store,write
在这里插入图片描述
lock:作用于主内存的变量,把一个变量标识为一个线程独占的状态
read:作用于主内存的变量,把一个变量值从主内存中传输到工作内存中,以便进行load
load:作用于工作内存的变量,把read从主内存放入工作内存中的变量副本中
use:作用于工作内存的变量,把工作内存中的变量值传递过执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时将会执行这个操作
assign:作用于工作内存的变量,把一个从执行引擎接收到的值赋值给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作
store:作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中,以便执行后续的write操作
write:作用于主内存,它把store操作中的值传送到主内存的变量中

volatile关键字的作用为:
1.当一个变量声明为volatile后,对该变量执行写操作之后,JMM会把该线程中的变量的最新值刷新到主内存中
2.在变量刷新到主内存的同时会触发导致其他线程工作内存的那变量副本无效

JMM层面的“内存屏障”:

1.LOADLOAD屏障:对于这样的语句Load1 ; LOADLOAD; load2,在Load2及后续读取操作 要读取的数据被访问前,保障Load1要读取的数据被读取完毕(Load1从主内存中获取数据)
在这里插入图片描述
2.STORESTORE屏障:对于Store1; STORESTORE; Store2 Store2及后续写入操作执行前,保证Store1的写入操作对其他处理器可见(相当于一般作用在某个写操作后,使得该操作立刻更新回主内存)
在这里插入图片描述
3.LOADSTORE屏障:对于Load1; LOADSTORE; Store2 在Store2及后续写入操作被执行前,保证Load1要读取的数据读取完毕
在这里插入图片描述
4.STORELOAD屏障:对于Stroe1; STORELOAD; Load2, 在Load2及后续所有读取操作执行前,保证Store1的写入对所有处理器可见
在这里插入图片描述

volatile关键字在代码层面实现

在这里插入图片描述
被volatile修饰的变量其访问标志位:ACC_VOLATILE

访问变量的字节码为getfield
在这里插入图片描述
在bytecodeInterpreter.cpp中
putfield中有 cache->is_volatile()判断变量是否为volatile,cache是incre变量在缓存中的一个实例
在这里插入图片描述
Is_volatile()的源码在accessFlags.cpp中,判断变量是否有ACC_VOLATILE标志位
在这里插入图片描述
确定变量为volatile后,对变量是否为基础数据类型进行判断
elseif(tos_type==btos){
obj->release_byte_field_put(field_offset,STACK_INT(-1));

当变量类型为byte时
调用release_byte_field_put(field_offset,STACK_INT(-1))
在这里插入图片描述
最后调用了release(),JVM使用了cpp原语进行实现,调用了cpp的volatile关键字,
cpp中的volatile关键字用来修饰变量,通常用于语言级别的memory barrier,被volatile变量表示随时可能发生变化,每次使用时,都必须从变量对应的内存地址读取,编译器对操作该变量的代码避免激进的优化

调用release_byte_field_put(field_offset,STACK_INT(-1))完成后,还会接着调用
在这里插入图片描述
OrderAccess::storeload();
在这里插入图片描述
即调用了storeload 内存屏障
在这里插入图片描述
在这里插入图片描述
至此volatile的源码实现梳理完毕,总结:
被声明为volatile的变量在编译后会被标志为ACC_VOLATILE,在变量的getfiel()和putfile()中(即对变量进行读取或赋值保存时)会调用cpp原语的volatile关键字进行修饰,然后将值写入缓存,cpp中对volatile变量处理为使用volatile变量时,总是从内存中读取该变量数据,在调用putfiel()完成后,会调用JMM的storeload内存屏障,storeload会对cacheline进行锁定然后将该缓存写入主内存,同时告诉寄存器和编译该变量内容已经发生变化需要重新加载该变量

以上就是volatile实现线程可见性的原理

全文结束

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值