理解Java内存模型笔记

在并发编程网上看到这个深入理解java内存模型系列文章,文章一个系列图文并茂讲得都很不错,让我了解到了很多关于内存可见性、内存屏蔽指令一些知识。在此结合自己对《深入理解Java虚拟机》的一些理解对文章重点基础知识做个笔记。注:文章基于JSR-133内存模型

Java内存模型

如上图,Java的并发采用的是共享内存模型,Java线程之间的通信总是隐式进行,整个通信过程对程序员完全透明。下面将循序渐进的介绍。

1、Java内存模型抽象

内存模型抽象是Java虚拟机规范为实现让Java 程序在各个平台下都能达到一致性的内存访问效果而试图定义的。(此处的内存模型与Java 内存区域中相关的堆、栈不是一个层次的内存划分

如上图,在Java中,所有实例域、静态域和数组元素存储在主内存(原文堆内存,勉强对应的话也没问题)中,主内存在线程之间共享(本文使用“共享变量”这个术语代指)。局部变量,方法定义参数(Java语言规范称之为formal method parameters)和异常处理器参数不会在线程之间共享,它们不会有内存可见性问题,也不受内存模型的影响。

2、内存间交互操作、规则

原文并没有介绍这一十分繁琐的一系列操作、规则,而是针对voliatile 、final 的一些特殊规定做了很详细的介绍,不过我觉得这有助于更好理解那些特殊规则。

Java内存模型定义了8种操作来完成主内存和工作内存(本地内存)之间 的具体的交互,虽然之后被弃用但内存模型没有改变。虚拟机实现时必须保证下面提及的每一种操作都是原子的、不可再分的(对于double 、long类型某些操作在某些平台允许例外)

8种操作包括:lock(锁定M)、unlock(解锁M)、read(读取M->)、load(载入->W)、use(使用W)、assign(赋值W)、store(存储->M)、write(写入M)

//其中M代表作用于主内存,W代表作用于工作内存

此外还定义了一些基本规则(不包括volatile的特殊规定),如read必须load一起出现以完成从主内存读取数据到工作内存的操作。(规则不详细介绍,理解便可)

3、三大特征中可见性

Java 三个特征:原子性、可见性、有序性。通过上面提的基本操作、基本规则可大致的认为基本数据类型的访问读写具备原子性(更大规模的原子性保证有monitorenter、monitorexit–synchronized),所以直接跳过原子性来讲解可见性。

可见性指的是当一个线程修改了共享变量的值,其他线程也能够立即得知修改。

Java内存模型是通过在变量修改后将新值通过回主内存,在变量读取前从主内存刷新变量值这种依赖主内存的作为传递媒介的方式来实现可见性的。(volatile的特殊规则保证新值能够立即通过到主内存,使每次使用前立即从主内存刷新。普通变量则不能保证)

原文中还提到了final的可见性问题,并详细地说明了this引用逃逸,在此就不多加篇幅论述。

此外还有一个关键字也能实现可见性,即synchronized 。同步块的可见性是由“对一个变量执行unlock操作前,必须先把此变量同步到主内存中(执行store、write操作)”这条规则获得的。

4、重排序

重排序是理解下一种特征前必须要说明的。

在执行程序时为了提高性能,编译器和处理器常常会对指令做重排序。

数据依赖性:如果两个操作访问同一个变量,且这两个操作中有一个为写操作,此时这两个操作之间就存在数据依赖性。数据依赖分三种类型:写后读、写后写、读后写。(注:仅针对单线程和单处理器指令序列)

重排序分三种类型:

  1. 编译器优化的重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。
  2. 指令级并行的重排序。现代处理器采用了指令级并行技术(Instruction-Level Parallelism, ILP)来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。
  3. 内存系统的重排序。由于处理器使用缓存和读/写缓冲区,为了充分利用处理器内部的运算单元,处理器可能会对代码进行乱序执行(Out-Of-Order Execution)优化,再将乱序执行结果重组,保证该结果与顺序执行结果一致,但不保证各语句计算先后顺序与输入顺序一致。

编译器和处理器在重排序时,会遵守数据依赖性,编译器和处理器不会改变存在数据依赖关系的两个操作的执行顺序。(注:仅针对单线程和单处理器指令序列)

5、三大特征中有序性

Java程序中天然的有序性可以总结为一句话:如果在本线程内观察,所有的操作都是有序的,如果在一个线程中观察另一个线程,所有操作都是无序的。

这句话的前半段可以根据上面重排序理解:单线程中,若操作间存在数据依赖性则不改变执行顺序,若不存在,则有可能进行重排序,但保证操作结果和顺序执行结果一致。故而看似都是有序的。

后半句话则指“指令重排序”现象和“工作内存和主内存同步延迟”现象。

Java语言提供volatile 和synchronized 两个关键字保证线程间操作的有序性。volatile关键字除立刻刷新主内存共享变量以外,本身包含了禁止重排序的语义。(原文中有详细的volatile语义描述)

6、先行发生原则

为什么将这一原则放在最后呢!因为此原则容易望文生义,如果在没有之前那些知识的基础上首先介绍的话,容易让人的意识里一直埋下“在前面的就是会先发生的、先执行的”误解。这会让你在接触之后的知识里很痛苦,难以理解的矛盾。(智商如我就是这样过来的 XD)

先行发生原则是判断数据是否存在竞争、线程是否安全的主要依据。

先行发生关系:如果说操作A先行发生于操作B。其实就是说在发生B之前,操作A产生的影响能被操作B观察到,“影响”包括修改了主内存共享变量的值、发送信息、调用方法等。–《深入理解Java虚拟机》(没错,听起来好像很容易理解的样子)

原文中是通过“执行的结果”来代替上述的“产生的影响”。

那么可能会问一些问题

(1)怎么判断两个操作存在先行发生关系?(2)先行发生关系能给这两个操作带来什么?

(1)Java内存模型通过限制编译器和处理器(内存屏蔽指令)的重排序,实现了无需任何同步器协助就已经存在的一些先行发生关系(只介绍大意):

  • 程序次序规则:在一线程内,按控制流(不是代码)顺序,前面操作–>后面操作
  • 管程锁定规则:unlock操作–>时间顺序后同一个锁的lock操作。
  • volatile规则:同上,略
  • 线程启动规则:Thread对象start()–>此线程的每一个动作
  • 线程终止规则:线程中所有操作–>此线程终止检测(Thead.join()、isAlive())
  • 线程中断规则:对线程interrupt()方法调用–>中断检测代码
  • 对象终结规则:一个对象的初始化完成–>对象finalize()方法开始
  • 传递性:若操作A–>操作B,操作B–>操作C,则操作A–>操作C
    注: –> 表示先行发生于

(2)正如我们定义先行发生关系,回头再看一遍定义,然后看下面的小例子

i=1; //这个操作在线程A中执行,代号a

j=1; //这个操作在线程B中执行,代号b

i=2; //这个操作在线程C中执行,代号c
若线程A中操作a先行发生于线程B的操作b,则可以确定在线程B执行后,变量j的值一定等于1。需要的依据:一是根据先行发生原则,a操作结果可以被b操作观察。二是线程A操作后C未执行(假设)

在上面A、B先行发生关系基础上再假设线程C出现在A、B之间且操作c和操作b没有先行发生关系,那么j 的值是多少?答案是不确定。1,2都有可能,因为线程C对变量的影响可能被B观察到也可能不会。

此时线程B就存在读取过期数据的风险,不具备多线程安全性。

先行发生关系可以保障操作之间的顺序性,和线程安全性。

注:时间的先后顺序和先行发生原则之间基本没有关系(存在所谓的工作内存和和主内存的同步延迟),我们衡量并发安全问题的时候不要受到时间顺序的干扰,一切以原则为准

觉得不错不妨打赏一笔