说到 Java 内存模型,很多人马上想到的是 JVM 里面的方法区、堆等,这里所讲的内容更准确来说是 Java 线程内存模型。
1.现代计算机多核并发缓存架构
- 计算机系统处理任务主要是靠处理器(CPU)来进行运算的,而运算中又会涉及到数据,数据在哪呢,数据自然是存储在计算机内存中。
- 处理器的运算速度相比物理内存的读写速度要快得多
- 因此为了提高计算机的运算速度,现在的计算机系统为处理器添加了一层读写速度尽量接近处理器的高速缓存
2.Java内存模型——JMM
- Java 内存模型与 CPU 缓存模型类似,是基于 CPU 缓存模型建立起来的。Java 线程内存模型是标准化的,屏蔽掉了底层计算机的区别。
- 通过上面的图和前面的介绍,我们就很容易明白我们平常所说的多线程编程时遇到数据状态不一致的问题是怎么产生的。
3.JMM数据原子操作
-
工作内存与主内存会进行数据读写交互,这个读写交互具体实现细节则是由Java内存模型来控制的
-
Java内存模型为主内存和工作内存间的变量拷贝及同步写回定义了具体的实现协议,该协议主要由8种操作来完成
- read(读取):作用于主内存变量,把一个变量值从主内存传输到线程的工作内存中
- load(载入):作用于工作内存的变量,把通过read操作从主内存中得到的变量值放入工作内存的变量副本中。
- use(使用):作用于工作内存的变量,从工作内存读取数据计算
- assign(赋值):把计算好的值重新赋值到工作内存中
- store(存储):作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中
- write(写入):作用于主内存的变量,它把通过store操作从工作内存中得到的变量的值放入主内存的变量中。
- lock(锁定):作用于主内存的变量,把一个变量标识为一条线程独占状态。
- unlock(解锁):作用于主内存变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
-
以两个线程读取 initFlag 变量为例,第一个读取,第二个修改
-
Java内存模型对这8中操作还存在着其他的约束
- 只允许read和load、store和write这两对操作成对出现。
- 不允许线程丢弃它的最近的assign操作,即变量在工作内存中改变之后,必须同步回写到主内存。
- 不允许线程把没有经过assign操作的变量,同步回写到主内存。
- 一个新的变量只能在主内存中诞生,不允许在工作内存中使用未经初始化的变量,即对一个变量 进行use、store操作之前,必须先执行过load、assign操作。
- 一个变量在同一时刻只能被一条线程执行lock操作,一旦lock成功,可以被同一线程重复lock多次, 多次执行lock之后,只有执行相同次数的unlock操作,变量才会被解锁。
- 对一个变量执行lock操作,将会清空工作内存中该变量的值,所以在执行引擎使用这个变量前,需要重新执行load或assign操作对其进行初始化。
- 对一个变量执行unlock操作之前,必须先把该变量同步回主内存(执行store、write操作)。
- 如果一个变量事先没有被lock操作锁定,那就不允许对它执行unlock操作,也不允许unlock一个被其他线程lock的变量。
4.JMM的3个特征
原子性
原子性(Atomicity)
Java内存模型直接用来保证原子性变量的操作包括use、read、load、assign、store、write,我们大致可以认为Java基本数据类型的访问都是原子性的(long,double除外)
如果用户要操作一个更大的范围保证原子性,Java内存模型还提供了lock和unlock来满足这种需求,但是这两种操作没有直接开放给用户,而是提供了两个更高层次的字节码指令:monitorenter 和 moniterexit,这两个指令对应到Java代码中就是synchronized关键字,所以synchronized代码块之间的操作具有原子性。
可见性
可见性(Visibility):当一个线程修改了变量之后,其他线程能立刻得知这个修改。
Java内存模型通过将变量修改后将新值同步写回主内存,在读取前从主内存刷新变量值,所以JVM内存模型是通过主内存作为传递介质来实现可见性的。无论是普通变量还是volatile修饰的变量都是这样的,唯一的区别就是volatile变量在被修改之后会立刻写回主内存,而在读取时都会重新去主内存读取最新的值,而普通变量则在被修改后会先存储在工作内存,之后再从工作内存写回主内存,而读的时候则是从工作内存中读取该变量的副本拷贝。
除了volatile可以实现可见性之外,synchronized和final关键字也能实现可见性。synchronized同步块的可见性是因为对一个变量执行unlock操作之前,必须将变量的改动写回主内存来(store、write两个操作)实现的。而final字段则是因为一旦final字段初始化完成,其他线程就可以访问final字段的值,而且final字段初始化完成之后就不再可变。
有序性
处理器在执行运算的时候,会对程序代码进行乱序执行优化,也叫做重排序优化。同样的,在JVM中也存在指令重排序优化,这种优化在单线程中是不会存在问题的,但如果这种优化出现在多线程环境中,就可能会出现多线程安全的问题,因为线程1的指令优化可能影响线程2中某个状态。
Java提供了volatile和synchronized关键字来保证线程间操作的有序性。volatile是因为其本身的禁止指令重排序语义来实现的,而synchronized则是由“同一个变量在同一时刻只能有一个线程对其进行lock操作”这条规则来实现的,这也就是synchronized代码块对同一个锁只能串行进入的原因。