可见性实现
可见性实现主要借助汇编lock前缀指令来实现。lock前缀会发出LOCK#信号指令执行分为两种情况:
变量所在内存缓存行(cache line)对齐:不加锁,直接修改主存值
变量所在内存缓存行(cache line)没有对齐:锁缓存行,修改主存值
MESI协议:当CPU写数据时,如果发现操作的变量是共享变量,即在其它 CPU 中也存在该变量的副本,会发出信号通知其它CPU将该内存变量的缓存行设置为无效。其他CPU通过总线嗅探协议,不停在嗅探总线上发生的数据交换,跟踪其他缓存在做什么,它们以此来使自己的缓存保持同步。
由于MESI协议需要不断的从总线嗅探和CAS循环,无效的交互导致总线带宽达到峰值,因此不推荐大量使用volatile关键字。
有序性实现
as-if-serial
happens-before规则:
1、程序次序原则
2、锁原则
3、volatile
4、线程启动原则
5、线程中断原则
6、线程终止原则
7、对象终结原则
8、对象终结原则
volatile的有序性主要基于以上原则来实现。
使用内存屏障禁止指令重排序:
内存屏障主要分为以下4类:
内存屏障插入策略:
在每个volatile写操作的前面插入一个StoreStore屏障。
在每个volatile写操作的后面插入一个StoreLoad屏障。
在每个volatile读操作的后面插入一个LoadLoad屏障。
在每个volatile读操作的后面插入一个LoadStore屏障。
那么volatile是怎么实现内存屏障的呢?
volatile是通过lock前缀指令实现内存屏障来禁止指令重排序的,lock前缀指令锁的是总线或者缓存行。
为什么不能保证原子性
i++的字节码如下:
public class Test {
public static void main(String[] args) {
MyData myData = new MyData();
myData.incre();
System.out.println(myData.getI());
}
}
class MyData {
volatile int i = 0;
public void incre() {
i++;
}
public int getI() {
return i;
}
}
javap -c MyData后查看字节码指令:
class cn.com.gome.scot.alamein.share.web.app.MyData {
volatile int i;
cn.com.gome.scot.alamein.share.web.app.MyData();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: iconst_0
6: putfield #2 // Field i:I
9: return
public void incre();
Code:
0: aload_0
1: dup
2: getfield #2 // Field i:I
5: iconst_1
6: iadd
7: putfield #2 // Field i:I
10: return
public int getI();
Code:
0: aload_0
1: getfield #2 // Field i:I
4: ireturn
}
可见,i++被编译程4条指令:
2: getfield #2 // Field i:I 从主内存拿到原始i
5: iconst_1
6: iadd // 执行i++进行i加1操作
7: putfield #2 // Field i:I // 把累加后的值写回主内存
三个线程同时通过getfield命令,拿到主存中的n值,然后三个线程,各自在自己的工作内存中进行加1操作,但他们并发进行 iadd 命令的时候,因为只能一个进行写,所以其它操作会被挂起,假设1线程,先进行了写操作,在写完后,由于volatile的可见性,应该需要告诉其它两个线程,主内存的值已经被修改了,但是因为太快了,其它两个线程,陆续执行 iadd命令,进行写入操作,这就造成了其他线程没有接受到主内存n的改变,从而覆盖了原来的值,出现写丢失