本文共 1994 字,大约阅读时间需要 6 分钟。
volatile常用于配合CAS和version实现锁机制,乐观锁。适用于读多写少模式。
1. volatile涉及与CPU与内存模型,线程的工作原理,网上找了一个经典图
CPU在轮询执行线程运算的时候,由于CPU的结构
CPU与内存交付有3级缓存机制,会导致缓存不一致性。CPU的缓存越靠近CPU内核core,速度越快 ,容量越小。
多核CPU对线程工作副本,多个线程对同一共享变量会有一致性的问题,导致线程不安全
多核CPU提供MESI保证缓存一致性
MESI(Modified Exclusive Shared Or Invalid)(也称为伊利诺斯协议,是因为该协议由伊利诺斯州立大学提出)是一种广泛使用的支持写回策略的缓存一致性协议,该协议被应用在Intel奔腾系列的CPU中,详见“support the more efficient write-back cache in addition to the cache previously used by the Intel processor”
--《大话处理器》
AMD演化出MOESI协议,Intel演化出MESIF协议,具体不细介绍
其中L1缓存又有LI(指令)和LD(数据)
数据交互如上图所示
如果一个共享变量被多个CPU核心所执行,结果将无法保证最终一致,参考锁的理论,保证任何多次重复运算结果一致。
2. volatile的作用
2.1 保证可见性
保证了多个线程对共享变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。
使用volatile关键字线程修改共享变量时会强制将修改的值立即写入主存,
并使其他线程的CPU的L1或者L2缓存中对应的缓存行无效(缓存锁)(CPU有缓存锁和总线锁),
总线锁:就是使用处理器提供的一个LOCK#信号,当一个处理器在总线上输此信号时,其他处理器的请求将被阻塞住,那么该处理器可以独占共享内存。
缓存锁:所谓“缓存锁定”是指内存区域如果被缓存在处理器的缓存行中,并且在Lock操作期间被锁定,那么当它执行锁操作回写到内存时,处理器不在总线上声言LOCK#信号,而是修改内部的内存地址,并允许它的缓存一致性机制来保证操作的原子性,因为缓存一致性机制会阻止同时修改由两个以上处理器缓存的内存区域数据,当其他处理器回写已被锁定的缓存行数据时,会使缓存行无效。
其他线程需要修改共享变量只能重新从主内存拉取,LOCK缓存锁
处理器为了提高处理速度,不直接和内存进行通讯,而是将系统内存的数据独到内部缓存后再进行操作,但操作完后不知什么时候会写到内存。
如果对声明了volatile变量进行写操作时,JVM会向处理器发送一条Lock前缀的指令,将这个变量所在缓存行的数据写会到系统内存。 这一步确保了如果有其他线程对声明了volatile变量进行修改,则立即更新主内存中数据。
但这时候其他处理器的缓存还是旧的,所以在多处理器环境下,为了保证各个处理器缓存一致,每个处理会通过嗅探在总线上传播的数据来检查 自己的缓存是否过期,当处理器发现自己缓存行对应的内存地址被修改了,就会将当前处理器的缓存行设置成无效状态,当处理器要对这个数据进行修改操作时,会强制重新从系统内存把数据读到处理器缓存里。 这一步确保了其他线程获得的声明了volatile变量都是从主内存中获取最新的。
2.2 禁止指令乱序重排
Java代码在编译和CPU运算的时候,指令会重排序,达到最优的运算能力,保证执行结果不变(happen-before原则)
int a=10;int b=5;a=a+b;//.....
比如上面的代码
可能优化后的执行结果是
int b=5;int a=10;a=a+b;//.....
volatile在编译的时候会禁止重排序
Lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成。
2.3不能保证原子性
比如如下的操作,假设i主存目前是10.
i++;
其实i++分为3步,即3条指令集,读取值i;i+1;i写主存
1. 线程1读取的时候
2.线程2执行i++,i=11
3.i=11刷新到线程1,但是线程1可能执行到第3步写主存了,然后i强制失效,拉取主存值11。
4.线程1写主存,i=11.相当于线程1的i++未执行。
根本原因:i++不能保证原子性
如何线程安全,比对缓存变量与主存的变量值是否相同,相同则更新,否则重试。复杂运算时,配合版本号使用,避免ABA问题;另外使用sync或者lock也行。
volatile在如下情况可保证线程安全性
参考文档:JVM高级特性