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

 2019-12-10 16:11  阅读(802)
文章分类:Java Core

* [物理机中的并发硬件效率与一致性][Link 1] * [Java线程执行的内存模型][Java] * [工作内存][Link 2] * [主内存][Link 3] * [内存间交互][Link 4] * [long和double的非原子性协定][long_double] * [Volatile类型变量的特殊规则和语义][Volatile] * [保证可见性][Link 5] * [禁止指令重排优化][Link 6] * [高效并发的原则][Link 7] * [可见性有序性和原子性][Link 8] * [先行发生Happens-Before][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)优化** > 若一个计算任务依赖另一计算任务的中间结果,那其顺序性,**不能靠代码的先后顺序来保证** ![20191210001641\_1.png][20191210001641_1.png] # Java线程执行的内存模型 # ![20191210001641\_2.png][20191210001641_2.png] Java虚拟机使用定义种Java内存模型,以屏蔽各种硬件和OS的内存访问差异,以实现让Java程序在各个平台下都能达到一致的并发效果。C/C++直接使用物理硬件和OS的内存模型。 **目标** 定义程序中各个变量的**访问规则**,即在虚拟机中将变量存储到内存和从内存中取出这样的底层细节。此处的变量包括**实例字段、静态字段和构成数组对象的元素**,但是不包括局部变量和方法参数,因为这些是线程私有的,不会被共享,所以不存在竞争问题 ![20191210001641\_3.png][20191210001641_3.png] ## 工作内存 ## 工作内存 * 每条线程都有自己的工作内存(可与高速缓存类比) * 线程读写变量,必须在自己工作的工作内存中进行 * 工作内存保存主内存变量的值的拷贝 * 不能直接读写主内存的变量 * 不同线程间,无法直接访问对方工作内存的变量 * 线程间变量值的传递,需要通过主内存 ## 主内存 ## 主內存 * 所有的变量存在主内存(虽然名字跟物理机的主内存一样,可类比,但**此主内存只是虚拟机内存的一部分**) ## 内存间交互 ## ![20191210001641\_4.png][20191210001641_4.png] 一个变量如何**从主内存拷贝到工作内存**、如何**从工作内存同步回主内存**,Java内存模型定义了8种操作。这些操作,都是原子操作 | Operation|Place|Instruction| | :-----: | :-----: | :-----: | | lock | MainMemory | 将变量标识为一条线程独占状态 | | unlock | MainMemory | 释放被锁定的变量,释放后的变量才能被其他线程锁定 | | read | MainMemory | 变量值从主内存读取到线程的工作内存,以便紧接着的load操作 | | load | WorkingMemeory | 将read操作得到变量值放入工作内存的变量副本中 | | use | WorkingMemeory | 将工作内存的变量值传递给线程执行引擎 | | assign | WorkingMemory | 將一个从执行引擎接收到的值,赋给工作内存的变量 | | store | WorkingMemory | 将工作内存中的一个变量的值,传送到主内存,以便紧接着的write操作 | | write | MainMemory | 将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并发编程的艺术 并发编程网 [Link 1]: https://blog.csdn.net/qq_33938256/article/details/52584863#物理机中的并发硬件效率与一致性 [Java]: https://blog.csdn.net/qq_33938256/article/details/52584863#java线程执行的内存模型 [Link 2]: https://blog.csdn.net/qq_33938256/article/details/52584863#工作内存 [Link 3]: https://blog.csdn.net/qq_33938256/article/details/52584863#主内存 [Link 4]: https://blog.csdn.net/qq_33938256/article/details/52584863#内存间交互 [long_double]: https://blog.csdn.net/qq_33938256/article/details/52584863#long和double的非原子性协定 [Volatile]: https://blog.csdn.net/qq_33938256/article/details/52584863#volatile类型变量的特殊规则和语义 [Link 5]: https://blog.csdn.net/qq_33938256/article/details/52584863#保证可见性 [Link 6]: https://blog.csdn.net/qq_33938256/article/details/52584863#禁止指令重排优化 [Link 7]: https://blog.csdn.net/qq_33938256/article/details/52584863#高效并发的原则 [Link 8]: https://blog.csdn.net/qq_33938256/article/details/52584863#可见性有序性和原子性 [Happens-Before]: https://blog.csdn.net/qq_33938256/article/details/52584863#先行发生happens-before [20191210001641_1.png]: https://gitee.com/chenssy/blog-home/raw/master/image/series-images/javaCore/javacore/20191210001641_1.png [20191210001641_2.png]: https://gitee.com/chenssy/blog-home/raw/master/image/series-images/javaCore/javacore/20191210001641_2.png [20191210001641_3.png]: https://gitee.com/chenssy/blog-home/raw/master/image/series-images/javaCore/javacore/20191210001641_3.png [20191210001641_4.png]: https://gitee.com/chenssy/blog-home/raw/master/image/series-images/javaCore/javacore/20191210001641_4.png

点赞(0)
版权归原创作者所有,任何形式转载请联系作者; Java 技术驿站 >> JVM之内存构成(二)--JAVA内存模型与并发

相关推荐