并发编程 - Java内存模型(JMM)

他规范了java虚拟机与计算机内存如何协调工作 ,他规定了一个线程如何及何时看到其他线程修改过的变量的值,以及在必须时,如何同步的访问共享变量。

jmm内存分配的概念

堆heap:
优点:运行时数据区,动态分配内存大小,有gc;
缺点:因为要在运行时动态分配,所以存取速度慢,对象存储在堆上,静态类型的变量跟着类的定义一起存储在堆上。

栈stack:
优点:存取速度快,仅次于寄存器,
缺点:数据大小与生存期必须是确定的,缺乏灵活性,栈中主要存放基本类型变量(比如,int,shot,byte,char,double,foalt,boolean和对象句柄),jmm要求,调用栈和本地变量存放在线程栈上
在这里插入图片描述
可以简单的认为主内存是java虚拟机内存区域中的堆,局部变量和方法参数是在虚拟机栈中定义的

主内存: java虚拟机规定所有的变量(不是程序中的变量)都必须在主内存中产生,为了方便理解,可以认为是堆区。可以与前面说的物理机的主内存相比,只不过物理机的主内存是整个机器的内存,而虚拟机的主内存是虚拟机内存中的一部分。

工作内存: java虚拟机中每个线程都有自己的工作内存,该内存是线程私有的为了方便理解,可以认为是虚拟机栈。

JMM概念

从抽象的角度来看,JMM 定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存(Main Memory)中,每个线程都有一个私有的本地内存(Local Memory),本地内存中存储了该线程以读/写共享变量的副本。本地内存是JMM 的一个抽象概念,并不真实存在。它涵盖了缓存、写缓冲区、寄存器以及其他的硬件和编译器优化。
在这里插入图片描述
在这里插入图片描述

Java 内存模型带来的问题

可见性问题

在这里插入图片描述
左边CPU 中运行的线程从主存中拷贝共享对象obj 到它的CPU 缓存,把对象obj 的count 变量改为2。但这个变更对运行在右边CPU 中的线程不可见,因为这个更改还没有flush 到主存中。

要解决共享对象可见性这个问题,我们可以使用volatile 关键字或者是加锁。

竞争问题

在这里插入图片描述
如果这两个加1 操作是串行执行的,那么Obj.count 变量便会在原始值上加2,最终主存中的Obj.count 的值会是3。然而图中两个加1 操作是并行的,不管是线程A 还是线程B 先flush 计算结果到主存,最终主存中的Obj.count 只会增加1 次变成2,尽管一共有两次加1 操作。要解决上面的问题我们可以使用java synchronized 代码块

重排序

1. 重排序类型

除了共享内存和工作内存带来的问题,还存在重排序的问题:在执行程序时,为了提高性能,编译器和处理器常常会对指令做重排序。重排序分3 种类型。

  1. 编译器优化的重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。
  2. 指令级并行的重排序。现代处理器采用了指令级并行技术(Instruction-LevelParallelism,ILP)来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。
  3. 内存系统的重排序。由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。
2.数据依赖性

数据依赖性:如果两个操作访问同一个变量,且这两个操作中有一个为写操作,此时这两个操作之间就存在数据依赖性。数据依赖分为下列3 种类型,上面3 种情况,只要重排序两个操作的执行顺序,程序的执行结果就会被改变。

3. as-if-serial

as-if-serial 语义的意思是:不管怎么重排序(编译器和处理器为了提高并行度),(单线程)程序的执行结果不能被改变。编译器、runtime 和处理器都必须遵守as-if-serial 语义。

内存屏障

Java 编译器在生成指令序列的适当位置会插入内存屏障指令来禁止特定类型的处理器重排序,从而让程序按我们预想的流程去执行。

  1. 保证特定操作的执行顺序。
  2. 影响某些数据(或则是某条指令的执行结果)的内存可见性。

编译器和CPU 能够重排序指令,保证最终相同的结果,尝试优化性能。插入一条Memory Barrier 会告诉编译器和CPU:不管什么指令都不能和这条Memory Barrier 指令重排序。

Memory Barrier 所做的另外一件事是强制刷出各种CPU cache,如一个Write-Barrier(写入屏障)将刷出所有在Barrier 之前写入cache 的数据,因此,任何CPU 上的线程都能读取到这些数据的最新版本。

JMM 把内存屏障指令分为4 类
在这里插入图片描述

StoreLoad Barriers 是一个“全能型”的屏障,它同时具有其他3 个屏障的效果。现代的多处理器大多支持该屏障(其他类型的屏障不一定被所有处理器支持)。

happens-before

在Java 规范提案中为让大家理解内存可见性的这个概念,提出了happens-before 的概念来阐述操作之间的内存可见性。对应Java 程序员来说,理解happens-before 是理解JMM 的关键。

JMM 这么做的原因是:程序员对于这两个操作是否真的被重排序并不关心,程序员关心的是程序执行时的语义不能被改变(即执行结果不能被改变)。因此,happens-before 关系本质上和as-if-serial 语义是一回事。as-if-serial 语义保证单线程内程序的执行结果不被改变,happens-before 关系保证正确同步的多线程程序的执行结果不被改变。

1. 定义

用happens-before 的概念来阐述操作之间的内存可见性。在JMM 中,如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须要存在happens-before 关系。

两个操作之间具有happens-before 关系,并不意味着前一个操作必须要在后一个操作之前执行!happens-before 仅仅要求前一个操作(执行的结果)对后一个操作可见,且前一个操作按顺序排在第二个操作之前(the first is visible to and ordered before the second)

2. 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,那么Ahappens-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()操作成功返回。
  7. 线程中断规则:对线程interrupt 方法的调用happens-before 于被中断线程的代码检测到中断事件的发生。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值