并发编程中常常绕不开原子性、可见性与有序性的讨论。本文的标题中提到了volatile对可见性和有序性的保证,唯独没有提原子性,是因为volatile只有在特定场景下才会保证原子性,像volatile++ 这种复合的操作是不保证原子性的,当然对任意单个volatile变量的读/写是具有原子性的。
什么是原子性
原子(atomic)本意是不能被进一步分割的最小粒子,而原子操作意为不可被中断的一个或者一系列操作。
文章开头说volatile只有特定场景下才会保证原子性,这个特定场景就是在32位处理器里,对double和long型的变量的读写操作加了volatile修饰可以保证原子性。因为double和long都是64位,当jvm在32位处理器上运行时,可能会把64位的double/long型变量拆分为两个32位的写操作来执行,其他处理器可能读到一个“写了一半”的无效值,加了volatile就可以保证原子性。
内存屏障
可见性和有序性是基于各种内存屏障来实现的,先来看下有哪些内存屏障类型,以及可以解决那些因重排序引起的有序性问题。
屏障类型 | 重排序问题 | 说明 |
---|---|---|
LoadLoad | 一个处理器先执行L1读操作,再执行L2读操作,但其他处理器看到的是先L2后L1 | 确保L1数据的读先于L2 |
StoreStore | 一个处理器先执行w1写操作,再执行w2写操作,但其他处理器看到的是先w2后w1 | 确保w1对数据的写操作对其他处理器可见(刷到内存)先于w2 |
LoadStore | 一个处理器先执L1读操作,再执行w2写操作,但其他处理器看到先w2后L1 | 确保L1数据的读,先于w2的写操作刷到内存 |
StoreLoad | 一个处理器先执行w1写操作,再执行L2读操作,但其他处理看到先L2后w1 | 确保w1对数据的写操作对其他处理器可见(刷到内存)先于L2对内存数据的读 |
本文的重点来了,对于volatile修复的变量,在它的读/写操作前后都会加上不同的内存屏障。
- 在每个volatile写操作的前面插入一个StoreStore屏障,后面插入一个StoreLoad屏障,保证volatile写与之前的写操作指令不会重排序,写完数据之后立即执行flush处理器缓存操作将所有写操作刷到内存,对所有处理器可见。
- 在每个volatilie读操作的前面插入一个loadload屏障,保证在该变量读操作时,如果其他处理修改过,必须从其他处理器高速缓存(或者主内存)加载到自己本地高速缓存,保证读取到的值是最新的。然后在该变量读操作后面插入一个loadstore屏障,禁止volatile读操作与后面任意读写操作重排序。
volatile底层实现可见性与有序性原理
关于实现有序性,上面段落里有说明,主要通过对volatile修饰的变量的读写操作前后加上各种特定的内存屏障来禁止指令重排序来保障有序性的。
关于实现可见性,主要是通过 Lock前缀指令 + MESI缓存一致性协议来实现的。
对volatiile修饰的变量执行写操作时,JVM会发送一个Lock前缀指令给CPU,CPU在执行完写操作后,会立即将新值刷新到内存,同时因为MESI缓存一致性协议,其他各个CPU都会对总线嗅探,看自己本地缓存中的数据是否被别人修改,如果发现修改了,会把自己本地缓存的数据过期掉。然后这个CPU里的线程在读取改变量时,就会从主内存里加载最新的值了,这样就保证了可见性。
更多精彩请关注百家号