Java内存模型简介

Amdahl定律揭示可通过增加机器的核心数量提高单一核心的频率的方式,来提高机器的性能。对于编程来说,如何利用好多核是挖掘处理器性能的关键。并发编程是充分利用多核优势的最直接体现。

物理机并发处理模型简介

在介绍Java内存模型前,先简单介绍物理机对并发的处理方案,两者处理思想相似。
处理器在执行“计算”时,需要与内存进行交互,如读取运算数据、存储运算结果等,这个I/O操作是很难消除的(无法仅靠寄存器来完成所有运算任务)。由于计算机的存储设备与处理器的运算速度有几个数量级的差距,所以现代计算机系统都不得不加入一层读写速度尽可能接近处理器运算速度的高速缓存(Cache)来作为内存与处理器之间的缓冲:将运算需要使用到的数据复制到缓存中,让运算能快速运行,当运算结束后再从缓存同步回内存之中,这样处理器就无需等待缓存的内存读写。
基于高速缓存的存储交互解决了处理器与内存的速度矛盾,但也为计算机系统带来更高的复杂度,并且高速缓存引入也了一个新的问题:缓存一致性(Cache Coherence,高速缓存和内存数据的一致性)。此外,多处理器系统中,每个处理器除了拥有私有的高速缓存外,还共享同一主内存(Main Memory),多个处理器的运算任务涉及到同一块主内存区域时,将可能导致各自的缓存数据不一致。为解决上述两个一致性问题,需要各个处理器访问缓存时都遵循一些协议,在读写时要根据协议来进行操作,这类协议有MSI、MESI(Illinois Protocol)、MOSI、Synapse、Firefly及Dragon Protocol等。多处理器系统的并发处理模型如下:
请添加图片描述
除了增加高速缓存之外,为了使得处理器内部的运算单元能尽量被充分利用,处理器可能会对输入代码进行乱序执行(Out-Of-Order Execution)优化,处理器会在计算之后将乱序执行的结果重组,保证该结果与顺序执行的结果是一致的,但并不保证程序中各个语句计算的先后顺序与输入代码中的顺序一致,因此,如果存在一个计算任务依赖另外一个计算任务的中间结果,那么其顺序性并不能靠代码的先后顺序来保证。与处理器的乱序执行优化类似,Java虚拟机的即时编译器中也有类似的指令重排序(Instruction Reorder)优化

Java内存模型(Java Memeory Model,JMM)简介

Java内存模型的目标是屏蔽各种硬件和操作系统的内存访问差异,以实现多平台场景下达到一致的内存访问效果。在JDK 1.5以后,Java内存模型逐渐成熟和完善。
Java内存模型对外提供的接口还是程序中变量的访问规则,即虚拟机将变量存储到内存,以及从内存中获取变量。注意,这里的变量仅指非线程私有变量(不包括局部变量和方法参数),包括实例字段、静态字段和构成数组对象的元素。
Java内存模型规定所有的变量(实例字段、静态字段、构成数组对象的元素,不包括局部变量和方法参数)都存储在主内存(Main Memory)中。每个线程则拥有自己的工作内存(Working Memory)。在线程的工作内存中,保存该线程使用到的变量的主内存副本拷贝。Java内存模型如下:
请添加图片描述

内存交互操作规则

主内存与工作内存交互时,必须遵循交互协议,才能保证主内存与工作内存的一致性。Java内存模型定义8种原子操作和执行规则来实现这一点。这8种操作是:
(1) lock(锁定):作用于主内存的变量,它把一个变量标志为一条线程独占的状态。
(2) unlock(解锁):作用于主内存中的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
(3) read(读取):作用于主内存的变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用。
(4) load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中。
(5) use(使用):作用于工作内存的变量,它把工作内存中一个变量的值传递给执行引擎。
(6) assign(赋值):作用于工作内存的变量,它把一个从执行引擎接受到的值赋给工作内存的变量。
(7) store(存储):作用于工作内存的变量,它把工作内存中一个变量的值传送到主内存中,以便随后的write操作使用。
(8) write(写入):作用于主内存中的变量,它把store操作从主内存中得到的变量值放入主内存的变量中。
对应执行流程如下:
请添加图片描述
8种基本操作对应的执行规则比较繁琐,这里不再介绍,后续会学习其等效判断原则————先行发生原则,来确定一个访问在并发环境下是线程安全的。

原子性、可见性、有序性

Java内存模型是围绕着并发过程中如何处理原子性、可见性和有序性这三个特征来建立。

原子性(Atomicity)

Java中基本数据类型的读取和赋值操作是原子性操作。所谓原子性操作就是指这些操作是不可中断的,要么一定做完,要么就没有执行。 比如:
i = 2; // 赋值操作,是原子性操作
j = i; // 读取i的值,然后再赋值给j, 2步操作
i++; // 读取i的值,加1,再写回主存,3步操作
有个例外是,虚拟机规范中允许对64位数据类型(long和double),分为2次32位的操作来处理,但是最新JDK实现时还是实现了原子操作的。 JMM只实现了基本的原子性,诸如i++那样的操作,必须借助于synchronized或Lock对象来保证整个代码块的原子性
综上,在Java中只有以下两类场景能保证原子性:
(1) 基本数据类型的读取和赋值操作是原子性操作;
(2) lock操作和unlock操作之间的操作是原子性操作。由于lock和unlock操作未直接开放给用户,可使用更高层次的字节码指令monitorenter和monitorexit隐式调用这两个操作。对应Java代码,就是synchronized代码块是原子性操作Lock对象修饰的代码块是原子性操作

可见性(Visibility,也称并发可见性)

可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即得知修改的值。
Java内存模型是通过在变量修改后将新值同步到主内存,在变量读取前从主内存刷新变量值规则,来实现可见性的。
对于volatile修饰的变量,可以保证可见性。(volatile变量具有可见性,能保证新值立即同步到主内存,以及每次使用前立即从主内存刷新)
除了volatile外,synchronized和final关键字也可实现可见性。synchronized同步块的可见性是由“对一个变量执行unlock操作之前,必须先把此变量同步回主内存中”这个规则保证。
final关键字的可见性则是通过“被final修饰的字段,在构造器中完成初始化后,如果构造器没有把this的引用传递出去,那么在其他线程中就能看见final字段的值”保证。
综上,在Java中有三类场景可保证可见性:
(1) volatile 关键字
(2) synchronized 关键字
(3) final 关键字

有序性(Ordering)

Java内存模型的有序性是指:如果在本线程内部观察,所有的操作都是有序的;如果在一个线程中观察另一个线程,所有的操作都是无序的。前半句是指“线程内表现为串行的语义(Within-Thread As-If-Serial Semantics)”,后半句是指“指令重排”现象和“工作内存与主内存同步延迟”现象。
Java提供volatile关键字和syncronized关键字保证线程间操作的有序性。
(1) volatile关键字本身包含禁止指令重排序的语义。
(2) syncronizd关键字通过“一个变量在同一时刻只允许一个线程对其进行lock操作”保证有序性。
综上,在Java中有两类场景可保证有序性:
(1) volatile 关键字
(2) synchronized 关键字

先行发生规则(Happens-before)

在介绍内存交互操作规则时,曾提到等效判断原则————先行发生原则,该原则用来确定一个访问在并发环境下是线程安全的。详细来说,“先行发生规则”是判断数据是否存在竞争、线程是否安全的主要依据。
先行发生是Java内存模型中定义的两项操作之间的偏序关系。如果说操作A先行发生于操作B,就是说A产生的影响能被B观察到,”影响“包括修改了内存中的共享变量值、发送了消息、调用了方法等。
Java内存模型定义一些先行发生关系,对于这些先行发生关系,无需任何同步器协助,可以直接使用。而对于不在此列的关系,就没有顺序性保障,虚拟机可以随意的进行重排。也就是说,先行发生原则能够保证有序性。这些先行发生规则是:
(1) 程序次序规则(Program Order Rule):在一个线程内,代码的书写顺序和执行顺序一致。总结来说,线程内表现串行语义。
(2) 管程锁定规则(Monitor Lock Rule):unlock 操作先行发生于后面对同一个锁的 lock 操作。
(3) volatile 变量规则(Volatile Variable Rule):对一个 volatile 变量的写操作先行发生于后面对这个变量的读操作。
(4) 线程启动规则(Thread Start Rule):线程对象 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。
注意,“时间先后顺序”和“先行发生原则”没有太大关联,“先行发生原则”并不要求“时间先后顺序”,而“时间先后顺序”也无法保证“先行发生原则”(指令重排序)。并发问题中,受“时间先后顺序”干扰时,必须以“先行发生原则”为准。

参考

《java并发编程实战》 Brian Goetz 等著 童云兰 等译

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值