JMM java内存模型 1.并发的关键,2.jmm内存模型,3.指令重排 4、happens-before

一.并发编程的两个关键性问题

1.1线程通信

通信是指线程间通过何种机制进行信息交换,在命令式编程中有两种方式,共享内存和信息传递

共享内存:共享内存通信是指线程间有公共的状态,通过对内存中公共状态的写-读达到通信的目的,这种方式是隐式的通信。

消息投递:消息投递的模式,没有共享内存的共同状态,所以线程间需要通过发送消息进行显式的通信。

1.2线程同步(这里的线程是指并发执行的活动实体)

同步是指不同线程间操作发生相对顺序的机制,在通信内存共享模式下,同步是显示进行的,程序员必须显式的对某个方法和代码进行互斥执行。

在消息投递模式中,是隐式的进行同步,因为发送消息一定是在接受消息前

总结:

在java中采用内存共享模型,所以他的通信是隐式的,所以对于我们来说都是黑盒的,所以在编写代码不规范的时候会出现很多内存可见性的问题。

二、JMM内存模型抽象结构

在java中所有的实例域(对象 堆内存),静态域(静态常量或者变量在方法区也就是线程共享区),数组(堆内存)堆内存在线程*享;局部变量(虚拟机栈)、方法定义参数

异常处理器不会在线程*享所以不会出现线程共享问题。

java 之间的通信采用内存模型也就是JMM,JMM决定了一个线程对共享变量的写入何时让其他线程可见,从抽象的反面看JMM定义了线程和内存的抽象关系:线程的共享变量放到main memory上,每个线程都有一个local memory,本地内存放了该线程共享变量的副本,本地内存是JMM的 一个抽象概念,并不真实存在。它涵盖了缓存、写缓冲区、寄存器以及其他的硬件和编译器优 化。Java内存模型的抽象示意如图3-1所示。

JMM java内存模型 1.并发的关键,2.jmm内存模型,3.指令重排 4、happens-before

从图3-1来看,如果线程A与线程B之间要通信的话,必须要经历下面2个步骤。

1)线程A把本地内存A中更新过的共享变量刷新到主内存中去。

2)线程B到主内存中去读取线程A之前已更新过的共享变量。 下面通过示意图(见图3-2)来说明这两个步骤。

JMM java内存模型 1.并发的关键,2.jmm内存模型,3.指令重排 4、happens-before

如图3-2所示,本地内存A和本地内存B由主内存*享变量x的副本。假设初始时,这3个 内存中的x值都为0。线程A在执行时,把更新后的x值(假设值为1)临时存放在自己的本地内存 A中。当线程A和线程B需要通信时,线程A首先会把自己本地内存中修改后的x值刷新到主内 存中,此时主内存中的x值变为了1。随后,线程B到主内存中去读取线程A更新后的x值,此时 线程B的本地内存的x值也变为了1。

从整体来看,这两个步骤实质上是线程A在向线程B发送消息,而且这个通信过程必须要 经过主内存。JMM通过控制主内存与每个线程的本地内存之间的交互,来为Java程序员提供 内存可见性保证。

三、从源代码到指令重排:

原因:为了提升代码效率,我们的编译器、处理器会对我们代码进行重新排序,而重新排序有分为三种

1.编译器重排:编译器在不改变原来语义的前提下,对代码进行重排。

2.指令并行的重排:现代处理器采用指令级的并行技术,也就是将多条指令重叠执行,如果不存在数据依赖,处理器可以改变语句对应机器码的执行顺序

3.内存的重排:因为处理器采用缓冲和读写缓存区,这使得加载和存储看上去是乱序的。

JMM java内存模型 1.并发的关键,2.jmm内存模型,3.指令重排 4、happens-before

问题:

这些指令重排会导致在多线程的内存可见性,对于处理器JMM会禁止特定编译器的指令重排(不是所有编译器),对于处理器来说,我们会在java编译器生成指令序列时,插入内存屏障 memory barriers (int memory fence)通过memory barriees来禁止特定处理器的重排。

JMM属于语言级别的内存模型,可以他可以确保在不同的处理器和编译器上面,禁止特定的处理器和编译器进行重排。为内存可见性提供保障。

四、并发编程模型的分类

现代处理器使用写缓冲区临时保存向内存写入数据,写缓冲区可以保证指令流水线持续运行,它可以避免处理器暂时停顿向内存写入数据而产生的延迟,1.通过批处理的方式,处理写缓冲去,2.合并对一个内存地址的多次写,避免对中线的占用。这个特性会对内存操作执行产生很大的影响,处理器的读/写执行的顺序,不一定和内存实际发生的一样

 

JMM java内存模型 1.并发的关键,2.jmm内存模型,3.指令重排 4、happens-before

JMM java内存模型 1.并发的关键,2.jmm内存模型,3.指令重排 4、happens-before

这里处理器A和处理器B可以同时把共享变量写入自己的写缓冲区(A1,B1),然后从内存 中读取另一个共享变量(A2,B2),最后才把自己写缓存区中保存的脏数据刷新到内存中(A3, B3)。当以这种时序执行时,程序就可以得到x=y=0的结果。

从内存操作实际发生的顺序来看,直到处理器A执行A3来刷新自己的写缓存区,写操作 A1才算真正执行了。虽然处理器A执行内存操作的顺序为:A1→A2,但内存操作实际发生的顺 序却是A2→A1。此时,处理器A的内存操作顺序被重排序了(处理器B的情况和处理器A一样, 这里就不赘述了)。

这里的关键是,由于写缓冲区仅对自己的处理器可见,它会导致处理器执行内存操作的 顺序可能会与内存实际的操作执行顺序不一致。由于现代的处理器都会使用写缓冲区,因此

现代的处理器都会允许对写-读操作进行重排序。

 

JMM java内存模型 1.并发的关键,2.jmm内存模型,3.指令重排 4、happens-before

表3-2我们可以看出:常见的处理器都允许Store-Load重排序;常见的处理器都不允许对 存在数据依赖的操作做重排序。sparc-TSO和X86拥有相对较强的处理器内存模型,它们仅允 许对写-读操作做重排序(因为它们都使用了写缓冲区)。

·sparc-TSO是指以TSO(Total Store Order)内存模型运行时sparc处理器的特性。

·表3-2中的X86包括X64及AMD64。

·由于ARM处理器的内存模型与PowerPC处理器的内存模型非常类似,本文将忽略它。

·数据依赖性后文会专门说明。

为了保证内存可见性,java编译器在生成指令序列时,会插入内存屏障来禁止特定类型的处理器进行指令重排,JMM把内存屏障分为4类

JMM java内存模型 1.并发的关键,2.jmm内存模型,3.指令重排 4、happens-before

 

store load命令是一个全能屏障,他包括其他三个类型的特点,同时现代大多数处理器都是支持这种屏障的,缺点就是每一次执行这个屏障都要把缓冲区的数据都刷新到内存中buffer fully flush

四、happens-before介绍

https://openjdk.java.net/jeps/188 java 内存模型官网

happens-before是指可以java5以后用的JSR-133内存模型,在java中如果一个操作结果需要对另一个可见,那么他们两种需要happens-before的关系,这里的两个操作既可以在一个线程也可以在两个线程。

JMM java内存模型 1.并发的关键,2.jmm内存模型,3.指令重排 4、happens-before

happens-before的规则

·程序顺序规则:一个线程中的每个操作,happens-before于该线程中的任意后续操作。

·监视器锁规则:对一个锁的解锁,happens-before于随后对这个锁的加锁。

·volatile变量规则:对一个volatile域的写,happens-before于任意后续对这个volatile域的 读。

·传递性:如果A happens-before B,且B happens-before C,那么A happens-before C。

happens-before不是要前一个操作一定要在后一个操作之前执行,而是前一个操作对后一个操作可见,前一个的操作按顺序在后一个操作之前

目的:相当于一个中间层,它里面包含了一个或者多个处理器的重拍规则,它避免Java程序员为了理解JMM提供的内存 可见性保证而去学习复杂的重排序规则以及这些规则的具体实现方法。

上一篇:使用单节点构成2-3-4树


下一篇:极客时间《Java并发编程实战》---并发编程BUG的源头与Java如何解决可见性和有序性问题笔记