Java多线程的volatile底层实现原理
或许你经常被问到?
Volatile关键字有何作用?
实现这些作用的底层如何实现?
Volatile能够保障可见性、有序性?原子性吗?
前言
我们都知道,Java代码在编译后会变成Java字节码,字节码被类加载器加载到JVM里,JVM执行字节码,最终需要转化为汇编指令在CPU上执行,Java中所使用的并发机制依赖于JVM的实现和CPU的指令。
Volatile作用
Java语言规范第3版中对volatile的定义如下:Java编程语言允许线程访问共享变量,为了确保共享变量能被准确和一致地更新,线程应该确保通过排他锁单独获得这个变量。我们看下面的图来理解。2个CPU都要去操作主存中counter变量时,,他们读取主存中的变量counter到自己cpu cache中,然后操作数据。CPU1改变counter=7的操作,对CPU2是不可见的。
实现可见性
上面的例子,如果我们加上Volatile关键字,实际上底层是这么回事。
1)将当前处理器缓存行的数据写回到系统内存。
2)写回主存的操作会使在其他CPU里缓存了该内存地址的数据无效,其他CPU执行时,就需要重新从主存中获取数据。
实现禁止指令重排序
禁止指令重排序有没有什么例子?可以参考下我的另一篇文章: DCL的单例一定是线程安全的吗
Java内存模型其实是通过内存屏障(Memory Barrier)来实现的禁止指令重排序, 内存屏障之前的所有写操作都要回写到主内存,内存屏障之后的所有读操作都能获得内存屏障之前的所有写操作的最新结果(实现了可见性)。
屏障类型 | 指令示例 | 说明 |
---|---|---|
LoadLoad | Load1; LoadLoad; Load2 | 保证load1的读取操作在load2及后续读取操作之前执行 |
StoreStore | Store1; StoreStore; Store2 | 在store2及其后的写操作执行前,保证store1的写操作已刷新到主内存 |
LoadStore | Load1; LoadStore; Store2 | 在stroe2及其后的写操作执行前,保证load1的读操作已读取结束 |
StoreLoad | Store1; StoreLoad; Load2 | 保证store1的写操作已刷新到主内存之后,load2及其后的读操作才能执行 |
比如在对象instance进行写操作,之前加StoreStore,之后加StoreLoad。
虚拟机厂商的内存屏障(Memory Barrier)技术是遵循MESI协议的。其他不通的虚拟机厂商或许有其他技术,但是也需要遵循MESI协议。
MESI 是指4中状态的首字母。每个Cache line有4个状态,可用2个bit表示,它们分别是:
或许你会问为什么,这个实际上是主存、跟CPU中高速缓存的一种协议 MESI协议,我抛砖引玉,略说一点,内容很多,想了解的同学,可以自行百度
总结:volatile实现了Java内存模型中的可见性和有序性,它的这两大特性则是通过内存屏障来实现的,同时volatile无法保证原子性。(因为有序性是编译器优化带来的问题,可见性是缓存不一致带来的问题,而原子性,是线程切换带来的问题)
==========================================================================