深入剖析volitail关键字

背景

CPU缓存一致性问题:
由来:CPU的发展频率不断得到提升,但是计算机内存在访问速度上没有多大突破,因此CPU的处理速度和内存的访问速度之间的差距越来越大,CPU资源
受到大量限制,降低了CPU整体的吞吐量,为了解决这个问题,于是就开始在CPU和主存之间增加了缓存,现在缓存的数量可以增加到了3级。缓存的出现
提高了CPU的吞吐能力,但是也引入了缓存一致性问题。比如i++这个操作,在程序运行过程中,首先要将主内存中的数据复制一份存放到CPU缓存中,CPU
寄存器在进行计算的时候就直接到缓存中读取和存入,当整个过程运算结束之后再将缓存中的数据刷新到主存中去。
具体过程如下:
1)读取主内存的i到CPU Cache中
2)对i进行加一操作
3)将结果写回到CPU Cache中
4)将结果刷新到主内存中
i++在单线程的情况下不会出现任何问题,但是在多线程的情况下就会有问题,每个线程都有自己的工作内存(本地内存,相当于CPU中的Cache),变量i
会在多个线程的本地内存中存一个副本。如果同时有2个线程执行i++操作,假设i的初始值为0,每一个线程都从主内存中获取i的值存入CPU Cache中,然后
经过计算再写入主内存中,很有可能i在经过了两次自增之后结果还是1,这就是典型的缓存不一致性问题。
为了解决缓存不一致性问题,通常主流的解决办法有如下2种:
通过总线加锁的方式。
通过缓存一致性协议。
第一种方式常见于早期的CPU中,是一种悲观的实现方式,CPU和其他组件的通信都是通过总线来进行的,如果采用总线加锁的方式,则会阻塞其他CPU对
其他组件的访问,从而使得只有一个CPU能够访问这个变量的内存。这种方式效率低下,所以就有了第二种通过缓存一致性协议的方式来解决不一致的问题。
在缓存一致性协议种最为出名的是Intel的MESI协议,该协议保证了每一个缓存种使用的共享变量副本都是一致的,它的大致思想是,当CPU在操作Cache中
的数据时,如果发现该变量时一个共享变量,也就是说在其他的CPU Cache中也存在一个副本,那么进行如下操作:
1)读取操作,不做任何处理,只是将Cache中的数据读取到寄存器。
2)写入操作,发出信息通知其他CPU将该变量的Cache line置为无效状态,其他CPU在进行该变量读取时候不得不到主内存中再次获取。(volitail的可见性)

深入剖析volitail关键字

1.volatile关键字的语义

被volitail修饰的实例变量或者类变量具备如下两层语义:
保证了不同线程之间对共享变量操作时的可见性,也就是说当一个线程修改volitail修饰的变量,另外一个线程会立即看到最新的值。
禁止对指令进行重排序操作。

2.volitail保证可见性

Reader线程从主内存中获取init_value的值为0,并且将其缓存到本地工作内存中。
Updater线程将init_valued的值在本地工作内存中修改为1,然后立即刷新至主内存中。
Reader线程在本地工作内存中的init_value失效,因此需要到主内存中重新读取init_value的值。

3.volitail保证了顺序性

volatile会直接禁止JVM和处理器对volitail关键字修饰的指令重排序,但是对于volatile前后无依赖关系的的指令则可以随便怎么排序。
比如:
int x = 0;
int y = 1;
volitail int z = 20;
x++;
y–;
在语句volitail int z = 20之前,先执行x的定义还是先执行y的定义,我们并不关心,只要能百分之百保证到z=20的时候x=0,y=1,同理关于x的
的自增以及y的自减操作都必须在z=20之后发生。

4.volitail不保证原子性

private static volatile int i = 0;
多个线程对i执行i++;最后的结果肯定是错误的。
i++的操作其实是分三步组成的:
1).从主内存中获取i的值,然后缓存至线程的工作内存中
2).在线程工作内存中为i进行加1操作
3).将i的最新值写入主内存中
上述3操作单独每一个都是原子性操作,但是合起来就不是了,因为在执行过程中很有可能被其他线程打断。例如如下的操作情况:
1)假设此时i 的值为100,线程A要对变量i执行自增操作,首先它需要都主内存中读取i的值,可是此时由于CPU的时间片调度的关系,执行权切换到了
线程B,A线程进入了RUNNABLE状态而不是RUNNING状态。(就是说A读取了主内存中i的值之后,CPU切换到了线程B下)
2)线程B同样需要读取主内存中i的值,由于线程A没有对i做过任何修改操作,因此此时B获取到的i仍然是100.
3)线程B工作内存中为i执行了加1的操作,但是未刷新至主内存中。
4)CPU时间片的调度又将执行权给了线程A,A线程直接对工作线程中的100进行加1运算(因为A线程已经从主内存中读取了i的值),由于B线程并未写入i
的最新值,因此A线程工作空间的100不会失效。(即使变量i被volitail关键字修饰,但是B线程还没有对i的值进行修改刷新到主内存,所以A的本地工作
内存中的i的值不会失效,A不会去主存中读取i的值)
5)线程A将i=101写入主内存中
6)线程B将i=101写入主内存中(上面虽然线程A修改了主内存中i的值,使线程B中的工作内存中i的值失效了,线程B会立即到主内存中去读取最新的i的值
这里体现了可见性,B线程读取到了i=101之后的操作是把自己工作内存中改动i=101并刷新到主内存中去)

参考资料:Java高并发编程详解

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值