您的位置:首页 > 编程语言 > Java开发

JVM之内存构成(二)--JAVA内存模型与并发

2016-09-19 13:05 155 查看
物理机中的并发硬件效率与一致性

Java线程执行的内存模型
工作内存

主内存

内存间交互

long和double的非原子性协定

Volatile类型变量的特殊规则和语义
保证可见性

禁止指令重排优化

高效并发的原则
可见性有序性和原子性

先行发生Happens-Before

这部分内容,跟并发有关

我们知道,多任务处理,在现代操作系统几乎是必备功能。让计算机同时去做几件事情,不仅因为CPU运算能力太强大了,还有一个重要原因,CPU的运算速度远远高于它的存储和通信子系统的速度,大量时间耗费在磁盘I/O,网络I/O,数据库访问

虚拟机层面,如何实现多线程,多线程之间因数据共享或竞争而引发的一系列问题及解决方案

物理机中的并发–硬件效率与一致性

物理机遇到的并发与虚拟机中的情况,有不少相似之处,再扩展到分布式系统,我发现,其实也有不少相似之处。这之间有许多值得玩味的地方。

让计算机并发执行多个运算任务

这里面,不可能仅仅靠CPU计算就搞定的。CPU至少要跟内存交互,读取运算数据,存储运算结果,这个IO很难消除。当然,也无法仅仅靠CPU内的寄存器完成所有运算任务

CPU与存储设备之间的运算速度有几个数量级的差距,所以现代计算机系统都不得不加入一层读写尽可能接近CPU速度的高速缓存(Cache),作为内存与CPU间的缓冲。将运算需要的数据复制到缓冲,让运算快速进行,完后将缓存同步到内存。如此,CPU就无需等待缓慢的内存读写

在速度差距很大时,利用缓存来缓冲,用空间换时间;但同时会带来数据同步问题

引入了缓存一致性(Cache Coherence)问题

多核处理器里,每个CPU都有自己的高速缓存(一级、二级、三级),而它们又共享同一主内存。

当多个CPU的运算任务都涉及同一块主内存区域,可能导致各自的缓存数据不一致;数据同步回主存时,以谁的缓存数据为准呢?

为解决一致性问题,需要CPU访问缓存时都遵循一些协议,读写时,根据操作协议来。如MSI、MESI、MOSI、Synapse、Firefly、Dragon、Protocol

内存模型: 可以理解为,在特定操作协议下,对特定的内存或高速缓存进行读写访问的过程抽象

同时为了使得处理器充分被利用,CPU可能会对输入代码进行乱序执行(Out-of-Order Execution)优化,CPU会在计算后将结果重组,保证结果与顺序执行一致。Java虚拟机的即时编译器也有类似的指令重排序(Instruction Reorder)优化

若一个计算任务依赖另一计算任务的中间结果,那其顺序性,不能靠代码的先后顺序来保证



Java线程执行的内存模型



Java虚拟机使用定义种Java内存模型,以屏蔽各种硬件和OS的内存访问差异,以实现让Java程序在各个平台下都能达到一致的并发效果。C/C++直接使用物理硬件和OS的内存模型。

目标

定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出这样的底层细节。此处的变量包括实例字段、静态字段和构成数组对象的元素,但是不包括局部变量和方法参数,因为这些是线程私有的,不会被共享,所以不存在竞争问题



工作内存

工作内存

每条线程都有自己的工作内存(可与高速缓存类比)

线程读写变量,必须在自己工作的工作内存中进行

工作内存保存主内存变量的值的拷贝

不能直接读写主内存的变量

不同线程间,无法直接访问对方工作内存的变量

线程间变量值的传递,需要通过主内存

主内存

主內存

所有的变量存在主内存(虽然名字跟物理机的主内存一样,可类比,但此主内存只是虚拟机内存的一部分)

内存间交互



一个变量如何从主内存拷贝到工作内存、如何从工作内存同步回主内存,Java内存模型定义了8种操作。这些操作,都是原子操作

OperationPlaceInstruction
lockMain Memory将变量标识为一条线程独占状态
unlockMain Memory释放被锁定的变量,释放后的变量才能被其他线程锁定
readMain Memory变量值从主内存读取到线程的工作内存,以便紧接着的load操作
loadWorking Memeory将read操作得到变量值放入工作内存的变量副本中
useWorking Memeory将工作内存的变量值传递给线程执行引擎
assignWorking Memory將一个从执行引擎接收到的值,赋给工作内存的变量
storeWorking Memory将工作内存中的一个变量的值,传送到主内存,以便紧接着的write操作
writeMain Memory将store操作的变量值,放入主内存的变量中
变量从主内存复制到工作内存,顺序执行read和load

变量从工作内存同步到主内存,顺序执行store和write

long和double的非原子性协定

Nonatomic Treatment of double and long Variables

Java内存模型要求8个操作都具有原子性,但对64位的数据类型,long和double,模型定义了相对宽松

允许虚拟机将没有被volatile修饰的64位数据的读写操作,划分为2次32位的操作。

允许,并强烈建议,虚拟机将这些操作实现为原子性操作。

目前商用Java虚拟机几乎都选择把64位数据的读写作为原子操作来对待

编写代码时,一般不需为long或double专门声明为volatile

Volatile类型变量的特殊规则和语义

前面说过,Java内存模型,其实是定义读写内存变量的规则。

有些类型的变量比较特殊,除了上面所述的8个基本操作原则外,有特殊的规则。

特殊规则

read、load、use操作,须连续一起出现,每次use时,都从主内存read,工作内存load主内存的值,相当于每次use都从主内存中获取变量的最新值。保证能看见其他线程对变量的修改

assign、store、write操作,须连续一起出现,工作内存中的每次修改,须立刻同步回主内存。保证其他线程可以看到自己对变量的修改

两条线程,若A线程对变量a的use/assign操作,先于B线程对变量b的use/assign操作,那么A线程对a变量的read/write操作,先于B线程对变量b的read/write操作。该规则要求变量不被指令重排序优化,保证代码执行顺序与程序的顺序相同

特殊语义

保证可见性

禁止指令重排优化

保证可见性

volatile是轻量级的synchronized,在多CPU开发中,保证了共享变量的“可见性”。

指当一条线程修改了变量的值,新的值可以被其他线程立即知道

volatile只能保证可见性,但无法保证原子性,which is a necessity for synchronization.

因此,如果不符合下面两个规则的运算场景,我们需要通過加锁,如synchronized关键字和java.util.concurrent包下的原子类,来保证源自性。如果符合,volatile就能保证同步

运算结果不依赖当前值,或者能够确保只有单一线程修改变量的值

变量不需要与其他的状态变量共同参与不变约束

如下面的代码,就非常适合用volatile变量来控制并发

volatile boolean shutdownRequested;

public void shutdown() {
shutdownRequested = true;
}
//当shutdown()被调用时,能保证所有线程中执行的doWork()方法都停下来
public void doWork() {
while(!shutdownRequest) {
//do something
}
}


禁止指令重排优化

被volatile修饰的变量,多执行了
lock addl $0x0,(%esp)
操作

这个操作,相当于一个内存屏障(Memory Barrier/Memory Fence),意思是,重排序时,不能把后面的指令重排序到內存屏障之前的位置

lock addl $0x0,(%esp)
汇编指令,把ESP寄存器的值加0,这个是空操作。其作用,是使得本CPU的Cache写入内存,该写入动作,也会引起别的CPU或别的内核无效化(Invalidate)其Cache,相当于对Cache中的变量,做了一次如Java内存模型中的”Store且Write操作”。所以,通过这样一个空操作,可让volatile变量的修改,对其他CPU立即可见

硬件架构上讲,指令重排序,是指CPU采用了允许将多条指令不按程序规定的顺序,分开发送给各个相应电路单元处理,同时保证结果正确,与程序顺序执行的结果一致。

高效并发的原则

Java内存模型,围绕着并发过程中如何实现原子性、可见性和有序性,3个特征来建立。我们来看看哪些操作,实现了这些特征

可见性、有序性和原子性

原子性(Atomicity)

对基本数据类型的访问和读写是具备原子性的。

对于更大范围的原子性保证,Java内存提供lock,unlock操作,但未直接开发给用户使用

更高层次,可以使用字节码指令monitorenter和monitorexit来**隐式使用**lock和unlock操作。这两个字节码指令反映到Java代码中,就是同步块——synchronized关键字。因此synchronized块之间的操作也具有原子性。

可见性(Visibility)

当一个线程修改了共享变量的值,其他线程能够立即得知这个修改。Java内存模型是通过在变量修改后将新值同步回主内存,在变量读取之前从主内存刷新变量值来实现

可见性的。volatile的特殊规则保证了新值能够立即同步到主内存,每次使用前立即从主内存刷新。

synchronized和final也能实现可见性。unlock前,先同步数据到主存。final修饰的字段在构造器中一旦被初始化完成,并且构造器没有把this的引用传递出去,那么其他线程中就能看见final字段的值

有序性(Ordering)

Java程序的有序性可以总结为一句话,如果在本线程内观察,所有的操作都是有序的(线程内表现为串行的语义);如果在一个线程中观察另一个线程,所有的操作都是

无序的(指令重排序和工作内存与主内存同步延迟线性

先行发生(Happens-Before)

如果Java内存模型中所有的有序性,仅仅靠volatile和synchronized来完成,那么一些操作会很繁琐,但我们没有感觉得到,因为有happens-before原則。

该原则是判断数据是否存在竞争、线程是否安全的主要依据

先行原则
Java内存模型中定义的两项操作之间的偏序关系。如果操作A Happens-Before 操作B,意思是,B发生时,A产生的影响能被B观察到

//线程A中执行
i = 1;
//线程B中执行
j = i;
//线程C中执行
i = 2;


如果操作A和操作C之间,不存在先行发生关系,C出现在A和B之间,那么,C线程对变量j的修改,B线程不一定观察得到,此时,B读取到的数据可能不是最新的,不是线程安全的

Java内存模型中的先行发生

8条规则

程序次序规则(Program Order Rule)

一个线程内, 按照控制流顺序,写在前面的操作先行发生与写在后面的操作

管程锁定规则(Monitor Lock Rule)

一个unlock操作先行发生于后面对同一个锁的lock操作(就是拿到一个同步监视器的锁后,其他线程在这个锁被释放前,必须等待)

Volatile变量规则(Volatile Variable Rule)

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

线程启动规则(Thread Start Rule)

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

线程终止规则(Thread Termination Rule)

线程中的所有操作都先行发生于对此线程的终止检测

线程中断规则(Thread Interruption Rule)

对线程的interrupt()方法的调用,先行发生于被中断线程的代码检测到中断事件的发生

对象终结规则(Finalizer Rule)

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

传递性(Transitivity)

A先行发生于B,B先行发生于C,那么,A先行发生于C

时间上的先后,不等于“先行发生”。

一操作先行发生,推不出时间上先发生。有指令重排序存在。

时间先后顺序与happens-before基本没太大关系,衡量并发安全问题,一切以happens-before原则为准,不要受到时间顺序的干扰

推荐阅读

infoq 深入理解Java内存模型

infoq Java并发编程的艺术

并发编程网
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  jvm java 内存 操作系统