在Java并发知识体系中,JMM是一个非常重要的概念,掌握JMM可以让我们对多线程编程的理解更进一步。

一、JMM是什么?

JMM ,Java Memory Model,中文含义为Java内存模型。主要用于对多线程环境下内存操作的一系列规范,也可以称之为协议。

java 解析拼接marc_缓存

二、为什么会出现JMM?

现代计算机结构对提升同时,出现了一些副作用,JMM的出现就是为了解决这些问题。JMM就是从这个模式下抽象出来的一套模型,并根据缓存一致性协议来规范多线程间的内存共享和通信。

1、现代计算机结构

主要为了解决CPU和物理内存速度上的巨大差异而引入了CPU缓存环节速度差的问题,引入了CPU缓存这个中间层。

java 解析拼接marc_数据_02

当中,物理内存被称为主内存,CPU缓存被称为工作内存。工作内存只供本CPU使用,其他CPU不能读写。数据读取流程是:工作内存读取主内存的数据,CPU的寄存器读取工作内存的数据。如果CPU读取工作内存中找不到对应的数据,则工作内存向主内存进行读取后,CPU再读取。但是工作内存的大小有限,如果对于那些很大的数据(CPU缓存无法存储),CPU才向主内存直接读取。

2、工作内存细化

工作缓存(CPU高速缓存)可以分为三级:L1、L2、L3,从左到右速度越慢、空间越大、离CPU越远,对于多核CPU来说,L1和L2是该核CPU独享,L3是则是同一个CPU内部所有核模块共享的。

java 解析拼接marc_java 解析拼接marc_03

3、缓存行

工作内存向主内存读取数据,例如读取属性A的内存数据时,预测近期会对内存A附近的数据进行访问,因此为了提升整体效率,就把连同A一起整块内存数据进行读取,这块内存数据称之为缓存行。为了避免各个厂商自身对缓存行的定义不已,特别是大小方面,因此统一标准每个缓存行的大小为64个字节

java 解析拼接marc_数据_04

4、出现的问题

上述结构在单线程的情况下,解决了CPU和物理内存之间频率的差异导致,以及频繁占用I/O总线带宽等这两个问题的性能瓶颈,使得计算机的运算能力得到质的飞跃。

但是在多线程的环境下,却会造成诸多问题,如缓存一致性问题、指令重排导致结果无法预测。下面具体讲述这两个问题:

缓存一致性问题

从上面描述可以指导,CPU使用的是共享变量的副本,多个CPU之间会对副本值进行修改然后回写到主内存。以那个CPU的回写值为准,这个就需要CPU对缓存的操作遵循某些协议,但是不同的系统的协议也是有所差异。

指令重排问题

为了提升运行效率,程序指令在三处的进行重新排序。

第一种:编译器

编译器根据单线程环境下,语义不被改变的情况下,重新安排指令的执行顺序。例如:以下两条语句,调整顺序不改变执行效果。

a=b;

c=2;

第二种:CPU执行时

如果指令间不存在数据依赖,则CPU会通过并行运行去执行这些指令。那么那些指令存在数据依赖呢,主要有以下三种形式:

类型

实例

描述

写后读

a=10;

b=a;

写一个变量后,再读该变量的值

写后写

a=10;

b=20;

写一个变量后,再改变该变量的值

读后写

a=b;

b=1;

读取一个变量的值后,再改变该变量的值

大家也可以看出处在数据依赖的两个语句,如果更改运行顺序,那么计算结果就会改变。

第三种:内存系统

CPU使用缓存和读写缓存冲区,load和store这两个原子操作执行看起来混乱,会让主内存与CPU缓存的同步出现时间差。

三、JMM如何工作?

首先,我们了解JMM的规范内容

JMM规定了所有的变量都存储在主内存(Main Memory)中。每个线程还有自己的工作内存(Working Memory),线程的工作内存中保存了该线程使用到的变量的主内存的副本拷贝,线程对变量的所有操作(读取、赋值等)都必须在工作内存中进行,而不能直接读写主内存中的变量(volatile变量仍然有工作内存的拷贝,但是由于它特殊的操作顺序性规定,所以看起来如同直接在主内存中读写访问一般)。不同的线程之间也无法直接访问对方工作内存中的变量,线程之间值的传递都需要通过主内存来完成。

总的来说线程只可以直接操作工作内存,然后让工作内存与主内存进行数据同步。

其次,JMM以Java并发编程的三大特征(原子性、可见性、有序性)为基础建立的。

1)原子性

要求一个操作不能被打断,要么全部执行完毕,要么不执行。

JMM同步的八种操作,每种都是原子操作:

操作

含义

lock(锁定)

作用于主内存的变量,把一个变量标记为一条线程独占状态

unlock(解锁)

作用于主内存的变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定

read(读取)

作用于主内存的变量,把一个变量值从主内存传输到线程的工作内存中,以便随后的load动作使用

load(载入)

作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中

use(使用)

作用于工作内存的变量,把工作内存中的一个变量值传递给执行引擎

assign(赋值)

作用于工作内存的变量,它把一个从执行引擎接收到的值赋给工作内存的变量

store(存储)

作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中,以便随后的write的操作

write(写入)

作用于工作内存的变量,它把store操作从工作内存中的一个变量的值传送到主内存的变量中

这八种操作,使用规定的使用规则:

1

不允许read和load、store和write操作之一单独出现。read与load,store与write必须成对使用

2

不允许线程丢弃他最近的assign操作。即工作变量的数据改变了之后,必须告知主存

3

不允许一个线程将没有assign的数据从工作内存同步回主内存。即store前必须有assign。

4

一个新的变量必须在主内存中诞生,不允许工作内存直接使用一个未被初始化的变量。即use前必须有load。

5

一个变量同一时间只有一个线程能对其进行lock。指多次lock后,必须执行相同次数的unlock才能解锁

6

如果对一个变量进行lock操作,会清空所有工作内存中此变量的值,在执行引擎使用这个变量前,必须重新load或assign操作初始化变量的值

7

如果一个变量没有被lock,就不能对其进行unlock操作。也不能unlock一个被其他线程锁住的变量

8

对一个变量进行unlock操作之前,必须把此变量同步回主内存

 

2)可见性

要求当有线程对共享变量的修改,要马上被其他的线程立即感知到。

在计算机架构中,CPU对经过总线的数据进行监听,被称之为CPU嗅探。当CPU工作内存向主内存进行数据同步时,需要经过总线,那么总线就会记录下数据值,然后被其他CPU所监听到,如果涉及自身工作内存内的共享变量变化时,一旦主内存值被更新,其他CPU马上把工作内存中的对应缓存行设置为无效。下次使用到相关共享变量时,需要重新从主内存中读取并加载到本地工作内存中。

不过工作内存更新主内存的操作由CPU决定,并一定马上执行,因此可能CPUA准备同步主内存时等待CPU调度时,CPUB抢先更新主内存,这时候CPUA的工作内存就被强制设置为无效,让A的这次计算白费,从而有可能导致整个程序结果异常。

Java提供一个关键字volatile,被该关键字修饰的变量变更新后,马上从工作内存同步到主内存中,保证多线程下变量的可见性。

java 解析拼接marc_数据_02

3)有序性

有序性指多线程执行的结果和单线程条件下执行的结果一致。但是程序指令经过编译器、内存系统以及执行器的指令重排,多线程下会出现有序性被破坏的情况。

Java中,synchronized关键字可以保证修饰的代码范围内指令有序性。

而内部机制方面,happen-before原则可以保证多并发中的原子性、可见性以及有序性,内容如下:

(1)程序次序规则(Program Order Rule)

在一个线程内,按照控制流顺序,书写在前面的操作先行发生(Happens-before)于书写在后面的操作。注意,这里说的是控制流顺序而不是程序代码顺序,因为要考虑分支、循环等结构。

(2)管程锁定规则(Monitor Lock Rule)

一个 unlock 操作先行发生于后面对同一个锁的 lock 操作。这里必须强调的是 “同一个锁”,而 “后面” 是指时间上的先后。

(3)volatile 变量规则(Volatile Variable Rule)

对一个 volatile 变量的写操作先行发生于后面对这个变量的读操作,这里的 “后面” 同样是指时间上的先后。

(4)线程启动规则(Thread Start Rule)

Thread 对象的 start() 方法先行发生于此线程的每一个动作。

(5)线程终止规则(Thread Termination Rule)

线程中的所有操作都先行发生于对此线程的终止检测,我们可以通过 Thread 对象的 join() 方法是否结束、Thread 对象的 isAlive() 的返回值等手段检测线程是否已经终止执行。

 (6)线程中断规则(Thread Interruption Rule)

对线程 interrupt() 方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过 Thread 对象的 interrupted() 方法检测到是否有中断发生。

 (7)对象终结规则(Finalizer Rule)

一个对象的初始化完成(构造函数执行结束)先行发生于它的 finalize() 方法的开始。

 (8)传递性(Transitivity)

如果操作 A 先行发生于操作 B,操作 B 先行发生于操作 C,那就可以得出操作 A 先行发生于操作 C 的结论。

四、总结

JMM的出现,是针对计算机架构优化产生让CPU缓存与物理内存不一致的问题,而抽象出的一个逻辑内存模型。模型力求解决上述不一致的问题,并为开发者屏蔽底层硬件、操作系统的差异问题,为开发者通过统一的编程规范。