想要理解Java并发机制的底层实现原理,是跳不过volatile这个关键字的,所以我们要先理解volatile是什么,是如何实现的。

volatile是轻量级的synchronized,它在多处理开发中保证了共享变量的”可见性“,可见性是当一个线程修改一个共享变量时,另外一个线程能读到这个修改的值。

一、volatile的定义

Java语言规范第三版对volatile的定义如下:Java编程语言允许线程访问共享变量,为了确保共享变量能被准确和一致地更新,线程应该确保通过排他锁单独获得这个变量。Java语言提供了volatile,在某些情况下比锁要更方便。如果一个字段被声明成volatile,Java线程内存模型确保所有线程看到这个变量的值是一致的。

二、volatile的实现原理

在谈volatile之前,我先想谈谈实现原理,在《Java并发编程的艺术》中,涉及到底层的实现大都是从编译器和处理器两方面进行阐述的,所以我们(对于像我一样对计算机操作系统不太了解的人)一定要区分清楚是在哪个层面讨论的。

那么volatile是如何保证可见性的呢?当对volatile变量进行写操作时,我们来查看相应的汇编指令:

0X01a3deld: movb $0x0,0x1104800(%esi); 0x01a3de24: lock addl $0x0,(%esp);

看不懂汇编代码不要紧,我们只需要知道有volatile修饰的共享变量进行写操作时会多出第二行汇编代码——lock前缀的指令,这个lock前缀的指令在多核处理器下引发了两件事情:

  1. 将当前处理器缓存行的数据写回到系统内存。
  2. 这个写回内存的操作会使在其他CPU里缓存了该内存地址的数据无效。

这条指令是JVM向处理器发送的。从编译器层面分析很简单,就是多了一行lock前缀的指令;从处理器层面来讲,就是这条指令引起了上述的两件事情,那么我们从处理器层面来具体讲解volatile的这两条实现原则。

1.Lock前缀指令会引起处理器缓存写回内存

Lock前缀指令导致在执行指令期间,声明处理器的LOCK#信号(注意:区分编译器和处理器)。那么LOCK#信号在多处理器环境中确保在声言该信号期间,处理器可以独占任何共享内存,因为会该信号锁住总线或缓存。但现在的处理器中,LOCK#信号一般不锁总线,而是锁缓存,毕竟锁总线的开销比较大。

对于Intel486和Pentium处理,在锁操作时,总是在总线上声言LOCK#信号。

在P6和现在的处理器中,如果访问的内存区域已经缓存到处理器内部,则不会声言LOCK#信号。相反,它会锁定这块内存区域的缓存并回写到内存,并使用缓存一致性机制来确保修改的原子性,此操作被称为”缓存锁定“(就是锁缓存),缓存一致性机制会阻止同时修改由两个以上处理器缓存的内存区域数据。

PS:这里锁的缓存我觉得是指公用的缓存,而不是私用的缓存,当然处理器不同,它的处理器的内部结构不同,如Intel Core i7处理器的L3是所有核共享的,L1和L2是是单核私有的。个人见解,还希望大家多多指点。

2.一个处理器的缓存回写到内存会使其他处理器的缓存无效

IA-32处理器和Intel 64处理器使用MESI(修改、独占、共享、无效)控制协议去维护内部缓存和其他处理器缓存的一致性。在多处理器系统中,IA-32处理器和Intel 64处理器能嗅探其他处理器访问系统内存和它们的内部缓存。

处理器使用嗅探技术来保证它的内部缓存、系统内存和其他处理器的缓存的数据在总线上保持一致。例如,在Pentium处理器和P6 family处理器中,如果通过嗅探一个处理器来检测其他处理器打算写内存地址,而这个地址当前处于共享状态,那么正在嗅探的处理器将使它的缓存行无效,在下次访问相同内存地址时,强制执行缓存行填充。

总结

当volatile修饰的变量进行写操作时:

1.这个变量所在缓存行的数据回写到系统内存中

2.每个处理器通过嗅探总线上的数据来检查自己缓存的值是否过期了

3.如果发现自己缓存行对应的内存地址被修改,则将缓存行设为无效状态

4.自己对这个数据进行修改操作时,重新从系统内存中把数据读到处理器缓存里

参考

《Java并发编程的艺术》