JAVA并发编程(十)之JMM内存模型

一、JAVA内存模型

在了解JAVA内存模型前先对计算机CPU内存架构有一个了解 CPU缓存架构

1、JMM模型

概念:由于不同的硬件不同的操作系统在内存管理及缓存一致性的细节上存在差异,Java 内存模型来屏蔽掉各种硬件和操作系统的内存差异,达到跨平台的内存访问效果。JLS(Java语言规范)定义了一个统一的内存管理模型JMM

  • 由于JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存,用于存储线程私有的数据,而Java内存模型中规定所有变量(实例域、静态域和数组元素)都存储在主内存,主内存是共享内存区域,所有线程都可以访问;
  • 线程工作内存中存储的是该线程读/写共享变量的副本,线程对变量的所有操作(读取、赋值),都必须在工作内存中进行,操作完成后再将变量写回主内存,不同线程之间也无法直接访问对方工作内存中的变量;
  • JMM内存模型中线程通信采用的是共享内存模型,Java线程之间的通信是隐式进行,整个通信过程对程序员完全透明,所以在并发编程是我们要考虑该过程。

线程A与线程B之间通信的步骤:

  1. 线程A把本地内存A中更新过的共享变量刷新到主内存中去
  2. 线程B到主内存中去读取线程A之前已更新过的共享变量

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

2、JMM内存模型的八大原子操作:

  1. lock(锁定):作用于主内存,它把一个变量标记为一条线程独占状态;
  2. read(读取):作用于主内存,它把变量值从主内存传送到线程的工作内存中,以便随后的load动作使用;
  3. load(载入):作用于工作内存,它把read操作的值放入工作内存中的变量副本中;
  4. use(使用):作用于工作内存,它把工作内存中的值传递给执行引擎,每当虚拟机遇到一个需要使用这个变量的指令时候,将会执行这个动作;
  5. assign(赋值):作用于工作内存,它把从执行引擎获取的值赋值给工作内存中的变量,每当虚拟机遇到一个给变量赋值的指令时候,执行该操作;
  6. store(存储):作用于工作内存,它把工作内存中的一个变量传送给主内存中,以备随后的write操作使用;
  7. write(写入):作用于主内存,它把store传送值放到主内存中的变量中。
  8. unlock(解锁):作用于主内存,它将一个处于锁定状态的变量释放出来,释放后的变量才能够被其他线程锁定;

3、八种原子操作下的八种操作规则

  1. read和load,store和write必须同时出现,并且按照顺序执行
  2. 不允许线程丢弃最后一个assign操作,在进行assign操作后,必须进行store和write
  3. 如果一个线程的工作内存中的一个变量没有发生assign操作,则不能够发生store和write
  4. 在工作内存中,如果对一个变量使用use或者store操作,则必须先执行assign和load操作
  5. 一个主内存中的变量在同一时刻只能被一个线程进行lock操作,但是这个线程可以进行多次lock操作,并且执行相应次数的unlock操作后,变量才会被解锁.
  6. 如果对一个量进行lock操作,将会清空此变量在其他线程工作内存中的值,这些线程在使用之前必须进行load或assign操作
  7. 一个线程如果没有对一个变量进行lock操作,则这个线程也不能对这个变量进行unlock操作
  8. 一个线程在进行unlock操作之前,必须先执行store和write操作

二、指令重排序

指令序列的重排序:在执行程序时,为了提高性能,编译器和处理器常常会对指令做重排序。重排序分3种类型
如图:JAVA源程序到最终执行指令的序列

1、编译器优化的重排序:编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。
2、指令级并行的重排序:现代处理器采用了指令级并行技术(Instruction-LevelParallelism,ILP)来将多条指令并行执行;指令级并行 ILP”的含义是:如果程序中相邻的一组指令是相互独立的,即不竞争同一个功能部件、不相互等待对方的运算结果、不访问同一个存储单元,处理器可以改变语句对应机器指令的执行顺序;
3、内存重新排序:这个跟之前两个不同的是,其为伪重排序,也就是说只是看起来像在乱序执行而已;
例如:以下两种情况

  • 两个内核同时访问主存相同的数据块,内核A从内存中读取,内核B对其进行写入,可能会迫使内核A等待内核B的将数据写入主存;等待中的内核A可能会选择提前运行其他内存指令,而不是傻等着;
  • CPU和主内存之间都具备一个高速缓存,高速缓存的作用主要为减少CPU和主内存的交互(CPU的处理速度要快的多),在CPU进行读操作时,如果缓存没有的话从主内存取,而对于写操作都是先写在缓存中,最后再一次性写入主内存;数据的写指令和读指令之间的乱序  例如:

这里处理器A和处理器B可以同时把共享变量写入自己的写缓冲区(A1,B1),然后从内存中读取另一个共享变量(A2,B2),最后才把自己写缓存区中保存的数据刷新到内存中(A3,B3),当以这种时序执行时,程序就可以得到x=y=0的结果。
从内存操作实际发生的顺序来看,直到处理器A执行A3来刷新自己的写缓存区,写操作A1才算真正执行了。虽然处理器A执行内存操作的顺序为:A1→A2,但内存操作实际发生的顺序却是A2→A1;此时,处理器A的内存操作顺序被重排序了(处理器B的情况与A相同);实质就是由于写缓冲区仅对自己的处理器可见,它会导致处理器执行内存操作的顺序可能会与内存实际的操作执行顺序不一致。

处理器提供了两个内存屏障指令(Memory Barrier)用于解决上述两个问题:

  1. 写内存屏障(Store Memory Barrier):在指令后插入Store Barrier,能让写入缓存中的最新数据更新写入主内存,让其他线程可见;强制写入主内存,这种显示调用,CPU就不会因为性能考虑而去对指令重排。
  2. 读内存屏障(Load Memory Barrier):在指令前插入Load Barrier,可以让高速缓存中的数据失效,强制从新的主内存加载数据;强制读取主内存内容,让CPU缓存与主内存保持一致,避免了缓存导致的一致性问题。

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

三、JAVA语言如何保证内存可见性

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

屏障类型指令示例说明
LoadLoadBarriersLoad1;LoadLoad;Load2该屏障确保Load1数据的装载先于Load2及其后所有装载指令的的操作
StoreStoreBarriersStore1;StoreStore;Store2该屏障确保Store1立刻刷新数据到内存(使其对其他处理器可见)的操作先于Store2及其后所有存储指令的操作
LoadStoreBarriersLoad1;LoadStore;Store2确保Load1的数据装载先于Store2及其后所有的存储指令刷新数据到内存的操作
StoreLoadBarriersStore1;StoreLoad;Load2

该屏障确保Store1立刻刷新数据到内存的操作先于Load2及其后所有装载指令的操作

它会使该屏障之前的所有内存访问指令(存储指令和访问指令)完成之后,才执行该屏障之后的内存访问指令

StoreLoad Barriers是一个“全能型”的屏障,它同时具有其他3个屏障的效果。现代的多处理器大多支持该屏障(其他类型的屏障不一定被所有处理器支持)。执行该屏障开销会很昂贵,因为当前处理器通常要把写缓冲区中的数据全部刷新到内存中(Buffer Fully Flush)。

四、JMM内存模型的两大原则

1、as-if-serial原则

as-if-serial语义保证单线程内程序的执行结果不被改变

2、happens-before原则

JMM设计的核心目标就是找到一个好的平衡点:

  1. 一方面,为程序员提供足够强的内存可见性保证,程序员希望内存模型易于理解、易于编程。程序员希望基于一个强内存模型来编写代码。
  2. 一方面,对编译器和处理器的限制要尽可能地放松;编译器和处理器希望内存模型对它们的束缚越少越好,这样它们就可以做尽可能多的优化来提高性能

JMM把happens-before要求禁止的重排序分为了下面两类:

  1. ·会改变程序执行结果的重排序。
  2. ·不会改变程序执行结果的重排序。

《JSR-133:Java Memory Model and Thread Specification》对happens-before关系的定义如下:

  1. 如果一个操作happens-before另一个操作,那么第一个操作的执行结果将对第二个操作可见,而且第一个操作的执行顺序排在第二个操作之前。
  2. 两个操作之间存在happens-before关系,并不意味着Java平台的具体实现必须要按照happens-before关系指定的顺序来执行。如果重排序之后的执行结果,与按happens-before关系来执行的结果一致,那么这种重排序并不非法(也就是说,JMM允许这种重排序)。

所以:16行与17行happens-before原则是不做要求的,但是17行与18行这种影响运算的happens-before原则就会产生作用

《JSR-133:Java Memory Model and Thread Specification》定义了如下happens-before规则:

  1. 程序顺序规则:一个线程中的每个操作,happens-before于该线程中的任意后续操作(该happens-before并非一定产生作用,这主要取决于指令重排序后是否影响单线程下的执行结果)
  2. 监视器锁规则:对一个锁的解锁,happens-before于随后对这个锁的加锁。
  3. volatile变量规则:对一个volatile域的写,happens-before于任意后续对这个volatile域的读。
  4. 传递性:如果A happens-before B,且B happens-before C,那么A happens-before C。
  5. start()规则:如果线程A执行操作ThreadB.start()(启动线程B),那么A线程的ThreadB.start()操作happens-before于线程B中的任意操作。
  6. join()规则:如果线程A执行操作ThreadB.join()并成功返回,那么线程B中的任意操作happens-before于线程A从ThreadB.join()操作成功返回。

参考《并发编程的艺术》

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值