1、volatile的特性
- 可见性:对一个volatile变量的读,总是能看到任意线程对这个volatile变量最后的写入。
- 原子性:对任意单个volatile变量的读/写具有原子性,但类似于volatile++这种复合操作不具有原子性(基于这我们会认为volatile不具有原子性)。而锁的互斥执行的特性可以确保对整个临界区代码的执行具有原子性。
- 有序性:对volatile修饰的变量的读写操作前后加上各种特定的内存屏障来禁止指令重排序来保障有序性。
2、volatile写-读的内存语义
- 当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量刷新到主内存。
- 当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效,线程接下来将从主内存中读取共享变量。
3、volatile可见性实现原理
- JMM内存交互层面实现
- volatile修饰的变量的read、load、use操作和assign、store、write必须时连续的,即修改后必须立即同步回主内存,使用时必须从主内存刷新,由此保障volatile变量对多线程的可见性。
- 硬件层面实现
- 通过Lock前缀指令,会锁定变量缓存行区域并写回主内存,这个操作称为"缓存锁定",缓存一致性机制会阻止同时修改被两个以上处理器缓存的内存区域数据。一个处理器的缓存回写到主内存会导致其他处理器的缓存无效。
4、指令重排序
java语言规范规定JVM线程内部维持顺序化语义。即只要程序的最终结果与它顺序化情况的结果相等,那么指令的执行顺序可以与代码顺序不一致,此过程叫指令的重排序。
指令重排序的意义:JVM能根据处理器特性(CPU多级缓存、多核处理器等)适当的对机器指令进行重排序,使机器指令能更符合CPU的执行特性,最大限度的发挥机器性能。
在编译器与CPU处理器中都能执行指令重排优化操作。
5、volatile重排序规则
volatile禁止重排序场景:
- 第二个操作是volatile写,不管第一个操作是什么都不会重排序。
- 第一个操作是volatile读,不管第二个操作是什么都不会重排序。
- 第一个操作时volatile写,第二个操作时volatile读,也不会发生重排序。
6、JMM内存屏障插入策略
- 1、在每个volatile写操作的前面插入一个StoreStore屏障。
- 2、在每个volatile写操作的后面插入一个StoreLoad屏障。
- 3、在每个volatile读操作的后面插入一个LoadLoad屏障。
- 4、在每个volatile读操作的后面插入一个LoadStore屏障。
7、JVM层面的内存屏障
- LoadLoad屏障:(指令Load1;LoadLoad;Load2),在Load2及后续 读取操作要读取的数据访问前,保障Load1要读取的数据被读取完毕。
- LoadStore屏障:(指令Load1;LoadStore;Store2),在Store2及后续写入操作被刷出前,保障Load1要读取的数据被读取完毕。
- StoreStore屏障:(指令Store1;StoreStore;Store2),在Store2及后续写入操作执行前,保障Store1的写入操作对其他处理器可见;
- StoreLoad屏障:(指令Store1;StoreLoad;Load2),在Load2及后续所有读取操作执行前保障Store1的写入对所有处理器可见。它的开销时四种屏障中最大的。在大多数处理器的实现中,这个屏障是个万能屏障;间距其他三种内存屏障的功能。
8、硬件层面内存屏障
硬件层提供了一系列的内存屏障memory barrier/memory fence来提供一致性的能力,拿X86平台来说,有以下几种内存屏障:
- lfence,是一种Load Barrier读屏障,在读指令前插入读屏障,可以让高速缓存中的数据失效,重新从主内存加载数据。
- sfence,是一种Store Barrier写屏障,在写指令之后插入写屏障,能让写入缓存的最新数据写回到主内存。
- mfence,是一种全能型的屏障,具备lfence和sfence的能力
- Lock前缀,Lock不是一种内存屏障,但是它能完成类似内存屏障的功能。Lock会对CPU总线或高速缓存加锁,可以理解为CPU指令级的一种锁。它先对高速缓存加锁,然后执行后面的指令,最后释放锁后会把高速缓存中的数据刷新回主内存。在Lock锁总线的时候,其他CPU的读写请求斗会被阻塞,直到锁释放。
不同硬件实现内存屏障的方式不同,Java内存模型屏蔽了这种底层硬件平台的差异,由JVM来为不同的平台生成相应的机器码。
9、内存屏障的两个能力:
- 阻止屏障两边的指令重排序
- 刷新处理器缓存