我在并发编程进程中经常会看到一个全局变量前面用volatile关键字进行润饰,所以去百度了一下这个关键字的用处,所以出现了:
锁???真的是这样吗?
那么咱们今日就来深度看看volatile究竟是什么。
CPU缓存模型
简单画了个图。
咱们的变量都是存储在内存中的,而cpu用于履行代码。在早期的核算机里当cpu需要去读取和修正某一个变量或数据时每次都需要去请求内存,这样就导致cpu的履行功率相对不高,所以引进了cpu缓存的概念。也就是在cpu中开辟一个cpu本地缓存,当cpu需要操作某个数据时假如是第一次读取那么就会从主内存中读取数据,然后保存到cpu本地缓存中,后续的查询和修正都将从cpu本地缓存中读取而不是主内存,这样就大大提高了cpu的履行功率。
但这样又会引发另一个问题。
图中cpu1将flag从主内存中读取,此刻flag=0,读取到cpu本地缓存之后对数据进行+1的操作,然后将flag=1的成果赋值给cpu1的本地缓存中。cpu2与cpu1进行同样的操作(读取主内存->加载到cpu2本地缓存->flag+1->更新cpu2本地缓存的值),主内存中的flag仍是=0,即使其他cpu也重复进行+1操作,flag一直仍是等于0。
问题也不难看出,cpu1和cpu2只是修正了他们自身的本地缓存,并没有将成果同步到主内存中,最终导致其他的线程去读取flag时都是flag的初始值。
CPU总线加锁和MESI协议
CPU总线加锁
CPU总线加锁机制是很早之前为处理上面出现的问题的一种办法,其原理是假如某个cpu要读取一个数据,那么会经过一个总线对这个数据进行加锁,导致只有这个cpu先操作完之后其他的cpu才能对这个数据进行读取。
这样做的缺点就是串行化了,导致cpu需要进行排队等候才能对数据进行操作,最终成果就是功率低下。
现在cpu总线加速现已没什么人用了。
MESI协议
MESI协议,缓存一致性协议
MESI协议处理上面出现的问题的办法是:假如cpu1对flag进行了修正,那么会将flag的修正记载刷新到主内存中,其他cpu会判别他们自身的本地缓存中是否拥有这个flag,假如拥有且被其他cpu进行了修正,那么他们自身的flag将失效,导致他们有必要去主内存再拿一份最新的flag(cpu嗅探机制)。
这样做根本处理了本地缓存和主内存参数不一致的问题,也避免了串行化的功率低下问题。
JAVA内存模型
java内存模型相比cpu缓存模型仍是有很多相似处的。
各阶段描绘
read:将数据从主内存中读取
load:将数据加载到作业内存中
use:从作业内存中读取并运用
assign:运用完后将参数赋值到作业内存
store:将参数从作业内存中取出并带到主内存
write:将参数赋值给主内存中的参数
假如flag被volatile润饰,那么在线程1assign将flag的值赋值给作业内存之后会当即store,将flag的数据刷入主内存,假如不被volatile润饰则不会进行store操作。
原子性、有序性、可见性
原子性:是指一个线程的操作是不能被其他线程打断,同一时间只有一个线程对一个变量进行操作。在多线程情况下,每个线程的履行成果不受其他线程的干扰。
根本上一个同享变量在多线程并发环境下根本是无法确保原子性的(除了上锁和原子类润饰)。
咱们常说volatile无法确保原子性,那么究竟为什么无法确保原子性,我简单描绘一下。这其实与java内存模型有关。
假如线程1和线程2一起将flag=0加载到作业内存中 (线程1)先对flag进行++操作,操作完之后再将flag=1的成果assign给作业内存
(线程2)也开端对flag进行++操作,线程2将作业内存中的flag进行use并在线程中完结++操作
(线程1)因为flag被volatile润饰所以作业内存中的flag被敏捷的store给主内存进行write
(线程2)因为线程2嗅探到flag的值现已被修复,作业内存中的flag=0现已失效,但因为线程2之前现已将flag=0读取到线程中进行操作,所以无法及时将之前的flag更改为最新的值,导致线程2核算完之后将flag=1再次刷入线程2的作业内存中。
最终导致被volatile润饰的flag并不能完结原子性操作。
可见性:某个线程修正了某一个同享变量的值,而其他线程是否能够看见该同享变量修正后的值。
关于可见性咱们上面现已做了描绘,经过MESI协议让各个cpu进行嗅探,假如发现变量被修正了则他们本地缓存的数据当即失效。
有序性:关于代码,一起还有一个问题是指令重排序,编译器和指令器,有的时分为了提高代码履行功率,会将指令重排序。
flag = false;
//线程1:
prepare(); // 预备资源
flag = true;
//线程2:
while(!flag){
Thread.sleep(1000);
}
execute(); // 基于预备好的资源履行操作
重排序之后,让flag = true先履行了,会导致线程2直接跳过while等候,履行某段代码,成果prepare()办法还没履行,资源还没预备好呢,此刻就会导致代码逻辑出现异常。
内存屏障
能够说volatile关键字之所以能确保有序性,与内存屏障有很大的关系。
LoadLoad屏障:Load1;LoadLoad;Load2,确保Load1数据的装载先于Load2后所有装载指令,他的意思,Load1对应的代码和Load2对应的代码,是不能指令重排的
Load1:
int localVar = this.variable
Load2:
int localVar = this.variable2
StoreStore屏障:Store1;StoreStore;Store2,确保Store1的数据一定刷回主存,对其他cpu可见,先于Store2以及后续指令
Store1:
this.variable = 1
Store2:
this.variable2 = 2
LoadStore屏障:Load1;LoadStore;Store2,确保Load1指令的数据装载,先于Store2以及后续指令
StoreLoad屏障:Store1;StoreLoad;Load2,确保Store1指令的数据一定刷回主存,对其他cpu可见,先于Load2以及后续指令的数据装载
关于volatile修正变量的读写操作,都会加入内存屏障
每个volatile写操作前面,加StoreStore屏障,制止上面的一般写和他重排;每个volatile写操作后边,加StoreLoad屏障,制止跟下面的volatile读/写重排
每个volatile读操作后边,加LoadLoad屏障,制止下面的一般读和voaltile读重排;每个volatile读操作后边,加LoadStore屏障,制止下面的一般写和volatile读重排
总结
综上可知,volatile并不是什么所谓的锁,而是一种完成了MESI协议的一种缓存一致性计划,经过cpu嗅探机制确保各个线程之间作业内存中同享变量的可见性。因为volatile的内存内存屏障也确保了volatile润饰的变量在读写进程防止指令重排序。