一、JMM内存模型

        JMM全称是Java内存模型,注意千万不要和JVM虚拟机内存也就是堆、栈这些搞混淆。其实严格意义来说应该在它名字中加上“线程”两个字,叫Java线程内存模型更合适。JMM的关键技术点都是围绕着多线程的原子性、可见性和有序性来建立的。

        JMM内存模型跟CPU缓存模型类似,是基于CPU缓存模型来建立的。我们先来看一下CPU缓存模型。

java jvm缓存实战_java

        早期CPU和主内存是直接交互的,但是CPU是非常快的,而主内存则相对较慢,所以现代计算机都会在CPU和主内存之间增加一层高速缓存。数据从硬盘加载到主内存后,再从主内存加载到CPU高速缓存,CPU是和CPU缓存进行交互,极大的提高了性能。 

        JMM内存模型的运作方式和CPU缓存模型类似,每个线程都有自己的工作内存,工作内存用的就是CPU高速缓存。共享变量加载到主内存,每个线程并不是直接操作主内存,而是将共享变量在自己的工作内存中复制一个副本,然后线程与自己的工作内存进行交互。这里每个线程的工作内存的工作方式和CPU缓存模型的工作方式一样。

java jvm缓存实战_jvm_02

        但这里有个问题,就是每个线程操作自己工作内存的共享变量,如果一个线程把某个变量改了,其它线程不知道,这就造成了数据不一致。要实现线程间数据同步,常用方式有两种,一种是使用synchronized关键字给代码块或方法加锁,另一种是使用volatile关键字修饰共享变量。本文重点是通过volatile关键字原理的分析,深入理解JMM。这就涉及JMM的一些底层内容。 

        JMM底层定义了一系列数据原子操作来完成多线程共享变量的管理。这些操作都是CPU硬件级别的操作。

java jvm缓存实战_java jvm缓存实战_03

        假设有共享变量initFlg,初始值为false。有两个线程都会使用这个变量。线程1读取initFlg过程:首先调用read操作将initFlg从主内存中读出;然后调用load操作将initFlg写入工作内存;线程调用USE操作从工作内存读取initFlg进行计算。

        线程2读取initFlg过程,也是一样步骤。如果线程2要改变initFlg的值,比如改为true,需要使用assign操作,将initFlg改变后的值重新写入工作内存。这时线程2的工作内存中initFlg被改为true。工作内存的值改变后,还要写回主内存,这步由store操作完成。这一操作只是将工作内存的数据写回到主内存,还没有修改主内存中initFlg的值。Write操作才是真正将store回的变量值赋给主内存中initFlg变量。

        那么如何将修改结果同步给线程1,并保证数据一致性呢?早期的方案是总线加锁。就是从调用read操作那一刻就给数据加锁,使其它线程无法读取数据,直到线程完成操作,才释放锁。这样做实际上是将对一个变量操作的并行执行的程序变成了串行,这样做效率太低,已经不再使用。

        目前使用的是MESI缓存一致性协议,这是一个硬件层面的协议。该协议规定,当多个CPU从主内存读取同一个数据到各自缓存,当其中某个CPU修改了缓存里的数据,将马上同步回主内存,其它CPU通过总线嗅探机制可以感知到数据的变化从而将自己缓存内的数据失效。

二、volatile实现原理

        MESI缓存一致性协议就是volatile底层实现的基础,当线程修改了工作内存的数据会立刻同步回主内存,而不是等线程执行完后再同步。同步回主内存会通过总线,其它线程通过总线嗅探机制监听着总线,从而感知到数据变化,让自己缓存中的数据失效。        

        当共享变量添加了volatile关键字修饰,源码被编译成汇编语言时,对共享变量的赋值语句会添加汇编lock指令。

        lock指令会做两件事:

        1、立即写回主内存,从而立刻触发store和write操作。

        2、回写主内存操作引起其它CPU里缓存了该内存地址的数据失效。

从而解决了共享变量的可见性问题。

从而并使加锁时间大幅降低,大大提高了并发性能。

volatile无法保证原子性,要保证原子性还是需要使用synchronized关键字。

从而保证有序性。