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内存模型,以屏蔽各种硬件和OS的内存访问差异,以实现让Java程序在各个平台下都能达到一致的并发效果。C/C++直接使用物理硬件和OS的内存模型。
目标
定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出这样的底层细节。此处的变量包括实例字段、静态字段和构成数组对象的元素,但是不包括局部变量和方法参数,因为这些是线程私有的,不会被共享,所以不存在竞争问题
每条线程都有自己的工作内存(可与高速缓存类比)
线程读写变量,必须在自己工作的工作内存中进行
工作内存保存主内存变量的值的拷贝
不能直接读写主内存的变量
不同线程间,无法直接访问对方工作内存的变量
线程间变量值的传递,需要通过主内存
所有的变量存在主内存(虽然名字跟物理机的主内存一样,可类比,但此主内存只是虚拟机内存的一部分)
一个变量如何从主内存拷贝到工作内存、如何从工作内存同步回主内存,Java内存模型定义了8种操作。这些操作,都是原子操作
变量从主内存复制到工作内存,顺序执行read和load
变量从工作内存同步到主内存,顺序执行store和write
Java内存模型要求8个操作都具有原子性,但对64位的数据类型,long和double,模型定义了相对宽松
允许虚拟机将没有被volatile修饰的64位数据的读写操作,划分为2次32位的操作。
允许,并强烈建议,虚拟机将这些操作实现为原子性操作。
目前商用Java虚拟机几乎都选择把64位数据的读写作为原子操作来对待
编写代码时,一般不需为long或double专门声明为volatile
有些类型的变量比较特殊,除了上面所述的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只能保证可见性,但无法保证原子性,which is a necessity for synchronization.
因此,如果不符合下面两个规则的运算场景,我们需要通過加锁,如synchronized关键字和java.util.concurrent包下的原子类,来保证源自性。如果符合,volatile就能保证同步
运算结果不依赖当前值,或者能够确保只有单一线程修改变量的值
变量不需要与其他的状态变量共同参与不变约束
如下面的代码,就非常适合用volatile变量来控制并发
这个操作,相当于一个内存屏障(Memory Barrier/Memory Fence),意思是,重排序时,不能把后面的指令重排序到內存屏障之前的位置
硬件架构上讲,指令重排序,是指CPU采用了允许将多条指令不按程序规定的顺序,分开发送给各个相应电路单元处理,同时保证结果正确,与程序顺序执行的结果一致。
对基本数据类型的访问和读写是具备原子性的。
对于更大范围的原子性保证,Java内存提供lock,unlock操作,但未直接开发给用户使用
更高层次,可以使用字节码指令monitorenter和monitorexit来**隐式使用**lock和unlock操作。这两个字节码指令反映到Java代码中,就是同步块——synchronized关键字。因此synchronized块之间的操作也具有原子性。
可见性(Visibility)
当一个线程修改了共享变量的值,其他线程能够立即得知这个修改。Java内存模型是通过在变量修改后将新值同步回主内存,在变量读取之前从主内存刷新变量值来实现
可见性的。volatile的特殊规则保证了新值能够立即同步到主内存,每次使用前立即从主内存刷新。
synchronized和final也能实现可见性。unlock前,先同步数据到主存。final修饰的字段在构造器中一旦被初始化完成,并且构造器没有把this的引用传递出去,那么其他线程中就能看见final字段的值
有序性(Ordering)
Java程序的有序性可以总结为一句话,如果在本线程内观察,所有的操作都是有序的(线程内表现为串行的语义);如果在一个线程中观察另一个线程,所有的操作都是
无序的(指令重排序和工作内存与主内存同步延迟线性)
该原则是判断数据是否存在竞争、线程是否安全的主要依据
先行原则
Java内存模型中定义的两项操作之间的偏序关系。如果操作A Happens-Before 操作B,意思是,B发生时,A产生的影响能被B观察到
如果操作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并发编程的艺术
并发编程网
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种操作。这些操作,都是原子操作
Operation | Place | Instruction |
---|---|---|
lock | Main Memory | 将变量标识为一条线程独占状态 |
unlock | Main Memory | 释放被锁定的变量,释放后的变量才能被其他线程锁定 |
read | Main Memory | 变量值从主内存读取到线程的工作内存,以便紧接着的load操作 |
load | Working Memeory | 将read操作得到变量值放入工作内存的变量副本中 |
use | Working Memeory | 将工作内存的变量值传递给线程执行引擎 |
assign | Working Memory | 將一个从执行引擎接收到的值,赋给工作内存的变量 |
store | Working Memory | 将工作内存中的一个变量的值,传送到主内存,以便紧接着的write操作 |
write | Main Memory | 将store操作的变量值,放入主内存的变量中 |
变量从工作内存同步到主内存,顺序执行store和write
long和double的非原子性协定
Nonatomic Treatment of double and long VariablesJava内存模型要求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并发编程的艺术
并发编程网
相关文章推荐
- 【Java并发编程】之十七:深入Java内存模型—内存操作规则总结
- JVM并发机制探讨—内存模型、内存可见性和指令重排序
- Java并发11:Java内存模型、指令重排、内存屏障、happens-before原则
- 内存屏障与JVM并发
- 【Java并发编程】:深入Java内存模型—内存操作规则总结
- JVM并发机制的探讨——内存模型、内存可见性和指令重排序
- JVM笔记1:Java内存模型及内存溢出
- JVM并发机制的探讨——内存模型、内存可见性和指令重排序
- 从JVM并发看CPU内存指令重排序(Memory Reordering) http://ifeve.com/jvm-memory-reordering/
- 二,JVM的内存模型,(GC)垃圾处理,调优监控(基于HotSpot VM,JDK1.5+)【JAVA内存模型】
- 内存屏障与JVM并发
- 【Java并发编程】:深入Java内存模型—内存操作规则总结
- 【Java并发编程】之十七:深入Java内存模型—内存操作规则总结 (r)
- JVM笔记1:Java内存模型及内存溢出
- 【Java并发编程】之十七:深入Java内存模型—内存操作规则总结
- JVM并发机制探讨—内存模型、内存可见性和指令重排序
- Java内存模型和JVM内存管理
- JVM并发机制探讨—内存模型、内存可见性和指令重排序
- 深入Java底层:内存屏障与JVM并发详解
- Oracle EBS 大量并发请求提交导致内存不足系统杀死Java JVM进程