volatile关键字的理解
volatile主要为了解决线程间变量的可见性问题,当某个线程修改公共变量时,该变量可以及时通过总线更新到使用这个变量的其它线程副本当中,每个线程都有自己的缓存副本,线程开始运行时就将数据读入到工作副本中。
加了volatile之后,底层的汇编指令会加个lock指令(相当于实现了一种内存屏障),lock指令可以基于总线锁或者缓存锁的机制来达到可见性的一个效果。这里面就涉及到缓存锁以及缓存一致性协议MESI,缓存锁的核心机制是基于缓存一致性协议来实现的,一个处理器的缓存会写到内存会导致其他处理器的缓存无效。
MESI协议存在的问题:就是各个CPU缓存行的状态是通过消息传递来进行的。如果CPU0要对一个在缓存中共享的变量进行写入,首先需要发送一个失效的消息给到其他缓存了该数据的 CPU。并且要等到他们的确认回执。CPU0在这段时间内都会处于阻塞状态。为了避免阻塞带来的资源浪费。CPU中又引入了store bufferes:
如上图,CPU0 只需要在写入共享数据时,直接把数据写入到 store bufferes中,同时发送invalidate消息,然后继续去处理其他指令(异步) 当收到其他所有 CPU 发送了invalidate acknowledge消息时,再将store bufferes中的数据数据存储至缓存行中,最后再从缓存行同步到主内存。但是这种优化就会引起指令重排序行为。例:
package com.zwx.concurrent;
public class ReSortDemo {
int value;
boolean isFinish; void cpu0(){
value = 10;//S->I状态,将value写入store bufferes,通知其他CPU当前value的缓存失效
isFinish=true;//E状态
}
void cpu1(){
if (isFinish){//true
System.out.println(value == 10);//可能为false
}
}
}
这时候理论上当isFinish为true时,value也要等于10,然而由于当value修改为10之后,发送消息通知其他CPU还没有收到响应时,当前CPU0继续执行了isFinish=true,所以就可能存在isFinish为true时,而value并不等于10的问题。
故而此时故而CPU层面就提供了内存屏障(Memory Barrier,Intel称之为 Memory Fence),使得软件层面可以决定在适当的地方来插入内存屏障来禁止指令重排序。
CPU内存屏障主要分为以下三类:
- 写屏障(Store Memory Barrier):告诉处理器在写屏障之前的所有已经存储在存储缓存(store bufferes)中的数据同步到主内存,简单来说就是使得写屏障之前的指令的结果对写屏障之后的读或者写是可见的。
- 读屏障(Load Memory Barrier):处理器在读屏障之后的读操作,都在读屏障之后执行。配合写屏障,使得写屏障之前的内存更新对于读屏障之后的读操作是可见的。
- 全屏障(Full Memory Barrier):确保屏障前的内存读写操作的结果提交到内存之后,再执行屏障后的读写操作。