java 中volitale 原理

5 篇文章 0 订阅
3 篇文章 0 订阅

前言

我们知道volatile关键字的作用是保证变量在多线程之间的可见性,它是java.util.concurrent包的核心,没有volatile就没有这么多的并发类给我们使用。

本文详细解读一下volatile关键字如何保证变量在多线程之间的可见性,在此之前,有必要讲解一下CPU缓存的相关知识,掌握这部分知识一定会让我们更好地理解volatile的原理,从而更好、更正确地地使用volatile关键字。

CPU缓存

CPU缓存的出现主要是为了解决CPU运算速度与内存读写速度不匹配的矛盾,。

为了解决cpu访问内存速度太慢的问题,现在CPU大多数情况下读写都不会直接访问内存,取而代之的是CPU缓存,CPU缓存是位于CPU与内存之间的临时存储器,它的容量比内存小得多但是交换速度却比内存快得多。而缓存中的数据是内存中的一小部分数据,但这一小部分是短时间内CPU即将访问的,当CPU调用大量数据时,就可先从缓存中读取,从而加快读取速度。

用一张图表示一下CPU-->CPU缓存-->主内存数据读取之间的关系:

当系统运行时,CPU执行计算的过程如下:

  1. 程序以及数据被加载到主内存
  2. 指令和数据被加载到CPU缓存
  3. CPU执行指令,把结果写到高速缓存
  4. 高速缓存中的数据写回主内存

如果服务器是单核CPU,那么这些步骤不会有任何的问题,但是如果服务器是多核CPU,那么问题来了,以Intel Core i7处理器的高速缓存概念模型为例(图片摘自《深入理解计算机系统》):

试想下面一种情况:

  1. 核0读取了一个字节,根据局部性原理,它相邻的字节同样被被读入核0的缓存
  2. 核3做了上面同样的工作,这样核0与核3的缓存拥有同样的数据
  3. 核0修改了那个字节,被修改后,那个字节被写回核0的缓存,但是该信息并没有写回主存
  4. 核3访问该字节,由于核0并未将数据写回主存,数据不同步

为了解决这个问题,CPU制造商制定了一个规则:当一个CPU修改缓存中的字节时,服务器中其他CPU会被通知,它们的缓存将视为无效。于是,在上面的情况下,核3发现自己的缓存中数据已无效,核0将立即把自己的数据写回主存,然后核3重新读取该数据。


如何解决缓存造成的数据不同步呢?


通过对代码进行编译、然后汇编最后发现

0x0000000002931351: lock add dword ptr [rsp],0h  ;*putstatic instance
                                                ; - org.xrq.test.design.singleton.LazySingleton::getInstance@13 (line 14)

标记了  volatile 的变量赋值时,前面会加上lock的这个符号,那lock这个符号是一个什么样的命令呢?
 

可以得出lock指令的几个作用:

  1. 锁总线,其它CPU对内存的读写请求都会被阻塞,直到锁释放,不过实际后来的处理器都采用锁缓存替代锁总线,因为锁总线的开销比较大,锁总线期间其他CPU没法访问内存
  2. lock后的写操作会回写已修改的数据,同时让其它CPU相关缓存行失效,从而重新从主存中加载最新的数据
  3. 不是内存屏障却能完成类似内存屏障的功能,阻止屏障两遍的指令重排序(不懂!)

关键是2点。
 

缓存一致性协议

讲缓存一致性之前,先说一下缓存行的概念:

  • 缓存是分段(line)的,一个段对应一块存储空间,我们称之为缓存行,它是CPU缓存中可分配的最小存储单元,大小32字节、64字节、128字节不等,这与CPU架构有关,通常来说是64字节。当CPU看到一条读取内存的指令时,它会把内存地址传递给一级数据缓存,一级数据缓存会检查它是否有这个内存地址对应的缓存段,如果没有就把整个缓存段从内存(或更高一级的缓存)中加载进来。注意,这里说的是一次加载整个缓存段,这就是上面提过的局部性原理

缓存一致性协议,就是要使多组缓存的内容保持一致。

缓存一致性协议有多种,但是日常处理的大多数计算机设备都属于"嗅探(snooping)"协议,

"嗅探(snooping)"协议

基本思想是:所有标记了lock的数据传输(内存和缓存之间)都发生在一条共享的总线上,而所有的处理器都能看到这条总线:缓存本身是独立的,但是内存是共享资源,这条共享总线一次只能一个线程使用,其他所有的核都会监控这个总线,当这个总线往内存写数据时,其他核监控到这个操作,并且查询下自己的缓存中是否存在这条记录,如果存在,就会将其标记为失效。

协议中,每个缓存行有4个状态,是:

 

由lock指令回看volatile变量读写

相信有了上面对于lock的解释,volatile关键字的实现原理应该是一目了然了。首先看一张图:

工作内存Work Memory其实就是对CPU寄存器和高速缓存的抽象,或者说每个线程的工作内存也可以简单理解为CPU寄存器和高速缓存。

那么当写两条线程Thread-A与Threab-B同时操作主存中的一个volatile变量i时,

Thread-A写了变量i,那么:

  • Thread-A发出LOCK#指令
  • 发出的LOCK#指令锁共享总线(或锁缓存行),同时让Thread-B高速缓存中的缓存行内容失效
  • Thread-A向主存回写最新修改的i

Thread-B读取变量i,那么:

  • Thread-B发现对应地址的缓存行被锁了,等待锁的释放,缓存一致性协议会保证它读取到最新的值

由此可以看出,volatile关键字的读和普通变量的读取相比基本没差别,差别主要还是在变量的写操作上。

和synchronized的比较

如果多个线程都会改的话,只能用synchronized 不能用volitale

原文地址http://www.cnblogs.com/xrq730/p/7048693.html,

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值