Java并发编程之JMM

一、什么是JMM?

  Java虚拟机规范定义了Java内存模型(Java Memory Model,JMM),用于屏蔽掉硬件和各种操作系统访问内存的差异,以期望Java程序在各种平台上都有一致的并发效果,Jvm规范了虚拟机和内存是如何协同工作的,规定了一个线程是如何和何时能够看到另一个线程修改共享变量后的值,以及在必要时是如何同步访问内存中大共享变量。JMM是一种抽象的概念、一组规则,而通过这组规则可以控制各个变量从共享数据区到私有数据区的访问方式。当然谈起JMM,就必然绕不开并发的三大特性:可见性、有序性、原子性,而JMM就是围绕这三大特性展开的。


主存和工作内存示意图

二、并发的三大特性是什么?

并发三大特性是可见性、有序性、原子性
下面将逐一分析每一个特性:
1.可见性
  可见性指的是当一个线程修改了共享变量的值,其它线程要能看到更改后的值。而在JMM中,则是通过修改共享变量的值,然后立即回写到主存中,这样其它线程从主存读取时就能获取最新的数据。
如何保证可见性?
在Java中,可以使用volatile关键字、内存屏障、synchronized关键字、Lock锁、final关键字。

2.有序性
  有序性指的是程序执行的顺序是按照代码执行的顺序执行。在程序执行时,JVM、编译器、处理器都存在指令重排序,会存在有序性问题。
如何保证有序性?
在Java中,可以使用volatile关键字、内存屏障、synchronized关键字、Lock锁。

3.原子性
  原子性指的是一个或多个操作,要么全部执行,并且在执行过程中不受任何因素的中断,要么全部不执行。
如何保证原子性?
在Java中可以使用CAS、synchronized关键字、Lock。

三、JMM和硬件内存架构的关系

  Java内存模型是一种抽象的概念,但是硬件架构却是实实在在的无力层面的东西,而它们之间也存在一定的差异。首先就是硬件内存并没有区分线程栈和堆,而是将所有线程栈和堆都分布在主存中(Main Memory),当然也有部分的线程栈和堆可能会出现在CPU寄存器以及高速缓存中。如下图所示,它们二者之间其实是一个交叉的关系:


JMM和硬件内存示意图

四、Java内存模型(JMM)详解

1.关于主存和工作内存之间的交互

关于主存和工作内存之间的交互,JMM规定了如下八种原子操作:
1.Lock(锁定):作用于主存的变量,将一个变量标识为一条线程独占的状态。
2.read(读取):作用于主存中的变量,将主存中变量的值传送到工作内存中。
3.load(加载):作用于工作内存中的变量,将read操作的值放入工作内存变量副本中。
4.use(使用):作用于工作内存中的变量,将工作内存中变量的值赋值给执行引擎,当虚拟机遇到需要使用变量值的字节指令时,将执行这个操作。
5.assign(赋值):作用于工作内存中的变量,执行引擎将接收到的值传给工作内存中的变量,当虚拟机遇到需要给变量赋值的字节指令时,将执行这个操作。
6.store(存储):作用于工作内存中的变量,将工作内存中变量的值传送到主存中。
7.write(写入):作用于主存中的变量,将store操作的值传送给主存中的变量。
8.unlock(解锁):作用于主存中的变量,将一个锁定状态的变量进行释放,释放后可以給其它线程进行锁定。

JMM规定执行上述8中原子操作时必须满足的条件:

  • 不允许read和load、store和write操作之一单独出现
  • 不允许一个线程丢弃最近的assign操作,变量在工作内存中改变了之后,必须将更改后的值写会主存中
  • 不允许线程没有发生assign操作,就将工作内存的值回写到主存中
  • 变量只能在主存中诞生,不允许在工作内存中使用未初始化的变量
  • 一个变量同一时刻只允许被一个线程lock,同一个线程可以lock多次,只有执行相同次数unlock,变量才会解锁
  • 对变量执行lock操作,会清空工作内存中此变量的值,需要重新执行load、assign进行初始化
  • 如果一个变量没有lock操作,那就不允许对它unlock操作
  • 对一个变量执行unlock之前,必须把变量同步到主存中
JMM八大原子操作
2.JMM内存可见性保证

Java程序的内存可见性保证可以分为以下三类:

  • 对于单线程程序而言,是不会出现可见性问题。编译器和处理器会共同确保单线程程序执行的结果和顺序一致性模型中执行的结果保持一致。
  • 对于正确同步的多线程而言,也不会出现可见性问题。JMM通过限制编译器和处理器的重排序,来为程序员提供内存访问的可见性。
  • 未正确同步的多线程程序,JMM为它们提供最小的安全保障:线程执行时读取到的值,要么是之前写入的值,要么是默认值,整体上是属于无需的,执行过的结果自然也无法预知。
3.volatile写、读的内存语义

对于volatile读:JMM会将工作内存中的变量置为无效,然后从主存中读取最新的值。
对于volatile写:JMM会将工作内存中变量的值立即回写到主存中。

4.volatile的特性

volatile的特性有可见性和有序性。
可见性:对于一个volatile修饰的变量的读,总是能读到这个变量最后写入的值。
有序性:对于一个volatile修饰的变量的读写操作,会在前后加上内存屏障禁止指令的重排序,用来保证有序性。

5.指令的重排序

程序执行的结果和顺序化结果一致,那么指令的执行顺序可以和代码的顺序不一致,此过程就称为指令的重排序。
指令重排序可以根据CPU的特性,适当的对指令进行重排序,以此来提升CPU性能,最大化发挥机器的性能。
相应的,关于volatile重排序的规则,可以看到如下图所示:

volatile禁止重排序

volatile禁止重排序的场景可以总结为如下信息:

  • 如果第二个操作是volatile写,那么不管第一个操作是什么都不会发生重排序
  • 如果第一个操作时volatile读,那么不管第二个操作是什么都不会发生重排序
  • 如果第一个操作是volatile写,而第二个操作是volatile读,则也不会发生重排序
6.内存屏障

内存屏障分为JVM层面和硬件层面。
对于Jvm层面的内存屏障,可以分为4种:
LoadLoad屏障:(指令Load1; LoadLoad; Load2),在Load2及后续读取操作要读取的数据被访问前,保证Load1要读取的数据被读取完毕。
LoadStore屏障:(指令Load1; LoadStore; Store2),在Store2及后续写入操作被刷出前,保证Load1要读取的数据被读取完毕。
StoreStore屏障:(指令Store1; StoreStore; Store2),在Store2及后续写入操作执行前,保证Store1的写入操作对其它处理器可见。
StoreLoad屏障:(指令Store1; StoreLoad; Load2),在Load2及后续所有读取操作执行前,保证Store1的写入对所有处理器可见。它的开销是四种屏障中最大的。在大多数处理器的实现中,这个屏障是个万能屏障,兼具其它三种内存屏障的功能

硬件层面的内存屏障:

  • Ifence,是一种Load Barrier读屏障
  • sfence,是一种Store Barrier写屏障
  • mfence,是一种全能型屏障,兼顾Ifencen和sfence的能力
  • Lock前缀指令,Lock前缀指令虽然不是内存屏障,但具有内存屏障的功能。

内存屏障的作用:
1.阻止屏障两边的指令重排序
2.刷新处理器的缓存

五、总结

  JMM其实就是一种抽象的概念、一组规则,通过这组规则控制变量从共享数据区到私有数据区的访问,JMM是围绕三大特性展开的。

你可能感兴趣的:(Java并发编程之JMM)