1、 JMM是什么

1.1 JMM简介

JMM:Java Memory Model,简称JMM,它是一种抽象的概念,并不真实存在。它描述了一组规范或者规则,通过这组规范定义了程序中各个变量(包括实例字段、静态字段和构成数组对象的元素等)的访问方式。
JMM规定了所有变量都存储在主内存(Main Memory)中。每个线程还有自己的工作内存(Working Memory),线程的工作内存中保存了该线程使用到的变量的主内存的副本考本,线程对变量的所有操作(读取、赋值等)都必须在工作内存中进行,而不能直接读写主内存中的变量。不同的线程之间无法直接访问对方工作内存中的变量,线程之间值的传递都需要通过主内存来完成。

1.2 主内存和工作内存(或称本地内存)

前面我们提到,JMM从抽象的角度定义了线程和主内存之间的抽象关系:
线程之间的共享变量存储在主内存(Main memory)中,每个线程都有一个私有的本地内存(Local memory),或者说工作内存(Working memory)。本地内存中存储量该线程的用以读/写共享变量的副本。本地内存是JMM的一个抽象概念,它并不真实存在。
按照我的理解,JMM中的主内存和工作内存,有点类似于JVM中线程共享和线程私有的部分:

  • JMM中主内存属于共享数据区,类似于包含着JVM中堆和方法区;所有线程创建的实例对象都存放在主内存中,不管该实例对象是成员变量还是方法中的局部变量,当然也包括共享的类信息、常量、静态变量。由于是共享数据去,多条线程同时对一个变量进行访问,存在线程安全问题。
  • JMM中工作内存属于线程私有的,类似于包含着JVM中的程序计数器、虚拟机栈和本地方法栈;它存放线程私有的数据,比如当前方法的所有本地变量信息(工作内存中存储着主内存中的变量副本拷贝),每个线程只能访问自己的工作内存,对其他线程时不可见的。就算两个线程执行的是同一段代码,他们也会各自在自己的工作内存中创建属于当前线程的本地变了。当然也包括了字节码行号指示器、相关Native方法信息等。注意,由于工作内存是每个线程的私有数据,线程之间无法相互访问工作内存,因此存储在工作内存的数据不存在线程安全问题。

主内存中的实例对象可以被多个线程共享,如果两个线程同时调用了同一个对象的同一个方法,那么两条线程需要将要操作的数据,考本一份到自己的工作内存中,执行完操作后,才刷新到主内存。
根据JVM虚拟机规范,主内存与工作内存的数据存储类型及操作方式:

  • 对于一个实例对象中的成员方法来说,如果方法中包含的局部变量时基本数据类型,那么将直接存储在工作内存的栈帧结构中;
  • 如果局部变量时引用数据类型,那么该局部变量的引用会存储在工作内存的栈帧中,而对象实例将存储在主内存(也即共享数据区域,堆)中;
  • 对于实例对象的成员变量,不管他事基本数据类型、基本数据类型的包装类还是引用数据类型,都会被存储到堆中;
  • static变量及类本身相关信息将会被存储在主内存中。

图示:
image.png
如图所示,线程A和B的工作内存中都有主内存中共享变量x的副本。如果线程A在执行时,更改了x的值为2,此时更改后的x值还是在线程A自己的工作内存中。如果线程A和线程B需要通信,线程A首先会把自己工作内存中修改后的x值刷新到主内存中,此时主内存x的值变为了2,随后,线程B从主内存中读取线程A更新后的A值,线程B中的x的值也变为了2。
也就是说,线程之间的通信过程必须要经过主内存。

1.3 JMM关于同步的规定

  1. 线程解锁前,必须把共享变量的值刷新回主内存;
  2. 线程加锁前,必须读取主内存的最新值道自己的工作内存;
  3. 加锁解锁必须是同一把锁。

1.4 再次理解线程间的安全问题

我们知道,多个线程操作同一个共享变量,可能会导致线程安全问题。这个线程安全问题的由来,根据JMM的相关规定,可以得出如下结果:
线程安全问题,其原因是JMM不允许工作线程直接操作主内存,只允许从主内存中将操作数据拷贝到各自线程的工作内存中,而工作内存只对当前线程可见,当有多个线程同时修改一个对象后,最后再写入主内存中就会造成结果不一致的情况。

2、JMM的三个特征

JMM是围绕并发编程中,可见性、原子性和有序性这三个特征建立的。

  • 可见性是指:一个线程对共享变量做了修改后,其他线程立即能够感知到该变量的变化。

JMM如何保证可见性:
JMM通过将在工作内存中的变量修改后的值同步到主内存,在读取变量前从主内存刷新最新值 到工作内存中,这种依赖主内存的方式来实现可见性的。
其实无论是普通变还是volatile变量都是这样子的,但是区别在于,volatile的特殊规则保证了volatile变量值修改后的新值立刻同步到主内存,每次使用volatile变量前立即从主内存中刷新变量值到工作内存中,因此volatile保证了线程之间操作变量的可见性,而普通变量不能保证这一点。
除了volatile关键字能实现可见性,synchronized、Lock、final也可以保证可见性。

  • 原子性一个操作不能被打断,要么全部执行完毕,要么不执行。也即:某个线程正在做某个具体业务时,中间不可以被加塞或者被分隔,需要整体完整,要么同时成功,要么同时失败。这有点类似于事务操作,要么全部执行成功,要么回退到执行该操作之前的状态。 (synchronized可以保证代码片段的原子性)。
  • 有序性:对于一个线程的代码而言,我们总是以为代码的执行是从前往后的依次执行的。在单线程程序里,代码确实是这样执行的。但是在多线程并发时,程序的执行就有可能出现乱序。用一句话总结为:在本线程内观察,操作都是有序的 ,如果在一个线程中观察另一个线程,所有的操作都是无序的。前半句指的是“线程内表现为串行语义”,后半句是指“指令重排”现象和“工作内存和主内存同步延迟”现象。

-> 什么是指令重排:
是指编译期和处理器在不影响单线程执行结果的前提下,对源代码的指令进行重新排序执行,这种重新排序是一种优化手段,目的是为了处理器内部的运算单元能尽量被充分利用,提升程序的整体运行效率。指令重排只能保证单线程执行下的正确性,在多线程环境下指令重排会带来一定的问题。
->指令重排的分类:

  1. - 编译期优化的重排序:编译期在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序;
  2. - 指令级并行的重排序:现代处理器采用了指令级并行技术来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。
  3. - 内存系统的重排序:由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱执行。

3、JMM内存屏障