Java 内存模型的基础(一)

一、并发编程模型的两个关键问题

在并发编程中,需要处理两个关键问题:线程之间如何通信及线程之间如何同步。

线程之间的通信有两种方式:共享内存和消息传递(信号量机制)。在共享内存的通信机制中,线程之间共享程序的公公状态,通过写-读内存中的公共状态进行隐式通信;而消息传递的通信机制中,线程之间没有共享的公共状态,线程之间必须通过发送消息显式进行的通信。

同步指程序中用于控制不同线程之间的操作发生的相对顺序共享内存的并发模型中,同步是显示进行的,程序员必须显式的指定那段程序需要在线程之间互斥进行。但是在消息传递的并发模型里,由于消息的发送必须在接收消息之前,因此同步方式是隐式的。

Java采用的是共享内存的并发模型,Java线程之间的通信总是隐式的进行的,整个通信过程对于程序员完全透明(也就是说让我们一丁点都不知道的意思!!!!!),因此想编写多线程程序的我们必须理解这些。

二、Java内存模型的抽象结构

在Java中,所有实例域、静态域和数组元素都存储在堆内存中,堆内存在线程之间是共享的;然而局部变量、方法定义的参数和异常处理器参数(都在栈里边进行存储),不会在线程之间共享,他们不会有内存可见性(一个线程对共享变量值的修改,能够及时地被其他线程察觉到)问题,也不受内存模型的影响。

Java之间的通信由Java内存模型(简称JMM)控制,JMM决定一个线程对共享变量的写入何使对另一线程可见。JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存,每个线程都有一个本地内存,用于存储改线程有读写权限的共享变量的副本。如下图所示:

《Java 内存模型的基础(一)》

如果线程A与线程B之间要进行通信的话,必须要经历以下两个步骤:

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

2、线程B到主内存中去读取线程A之前已经更改过的共享变量。

JMM通过控制主内存与每个线程的本地内存之间的交互,来为Java程序员提供内存可见性保证。

三、从源代码到指令序列的重排序

在执行程序时,为了提高性能,编译器和处理器常常回对指令做重排序。重排序分为三种类型:

1、编译器优化的重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。

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

3、内存系统的重排序。由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。

从Java源程序到最终的实际执行的指令序列,会分别经理下面三种重排序,如图所示:

《Java 内存模型的基础(一)》

上述的1属于编译器重排序,2和3属于处理器重排序,这些重排序可能会导致多线程程序出现内存可见性问题(在下面我们将会详细解释这个问题)。对于编译器,JMM的编译器的重排序规则会禁止特定类型的编译器重排序。对于处理器重排序,JMM会要求java编译器在生成指令序列时,插入特定类型的内存屏障指令,通过内存屏障指令来禁止特定类型的处理器进行重排序。

四、并发编程模型的分类

现代的处理器使用写缓冲区临时保存想内存写入的数据。写缓冲区可以保证指令的持续运行,他可以避免cpu等待IO造成的浪费。同时通过批处理的方式刷新写缓冲区,合并写缓冲区中对同意内存地址的多次写,减少总线的占用时间。虽然写缓冲好处极多,但是每个处理器上的缓冲区仅对当前处理器可见,这个特性回对内存操作的执行顺序产生重要的影响:处理器对内存的读/写操作的执行顺序,不一定与内存实际发生的读/写顺序一致!,请看如下示例:

 处理器1处理器2
代码

a = 1;

x = b;

b = 2;

y = a;

运行结果可能得到结果:x = y = 0

执行过程:

《Java 内存模型的基础(一)》

程序发生了重排序,可能在处理器A和处理器B读取内存之后才进行刷新操作。

前边说到了,为了保证内存可见性,Java编译器在生成指令序列的适当位置会插入内存屏障指令来禁止特定类型的处理器重排序。如下表所示:

屏障类型指令示例说明
LoadLoad BarriersLoad1;LoadLoad;Load2确保Load1数据的装载优先于Load及所有后续装在指令这装载
StoreStore BarriersStore1;StoreStore;Store2

确保Store1数据对其他处理器可见(刷新到内存)先于Store2以及后续存储指令的存储

LoadStore BarriersLoad1;LoadStore;Store2确保Load1数据装载优先于Store2以及后续的存储指令
StoreLoad BarriersStore1;StoreLoad;Load2确保Store1数据对其他处理器变得可见(刷新到内存)先于Load2以及后续装载指令的装载。StoreLoad Barriers会使该屏障之前的所有内存访问指令(存储和装载指令)完成之后,才执行该平战之后的内存访问指令。

StoreLoad Barriers是一个 “全能型“ 的屏障,它具有其他三个屏障的效果,现代大多处理器一般都支持该屏障。执行该屏障的开销会很昂贵,因为当前处理器通常要把写缓冲区中的数据全部刷新到内存中!!!!!!!!!

五、happens-before简介

从JDK5开始,Java使用心得JSR-133内存模型。JSR-133使用happens-before的概念来阐述操作之间的内存可见性。即在JMM中一个操作执行的结果需要对另一操作可见,那么两个操作之间必须要满足 happens-before 关系。这里提到的两个操作既可以使一个线程之内,也可以是在不同线程之间。具体规则如下:

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

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

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

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

 注意:两个操作之间具有happens- before关系,并不代表一个操作在另一个操作之前执行,仅仅代表前一个操作的结果对后一个操作可见,且前一个操作按顺序排在第二个操作之前。

happens- before 和 JMM的关系如下图:

《Java 内存模型的基础(一)》

 

 

    原文作者:java内存模型
    原文地址: https://blog.csdn.net/qq_39429962/article/details/84960841
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞