JMM内存模型

1、线程之间通信、同步

1.1、线程之间的通信

  1. 共享内存:线程之间通过写/读内存中的公共状态(共享对象)来隐式进行通信;
  2. 消息传递:线程之间通过明确的发送消息来显式进行通信。java中是wait()notify()

1.2、线程之间的同步

同步是指程序用于控制不同线程之间操作发生相对顺序的机制。

  1. 共享内存:同步是显式进行的,程序员必须显式指定某个方法或某段代码需要在线程之间互斥执行;
  2. 消息传递:由于消息的发送必须在消息的接收之前,因此同步是隐式进行的。

1.3、Java的并发采用的是共享内存模型

Java线程之间的通信总是隐式进行,整个通信过程对程序员完全透明。如果编写多线程程序的Java程序员不理解隐式进行的线程之间通信的工作机制,很可能会遇到各种奇怪的内存可见性问题。

2、JMM定义和产生原因

2.1、JMM定义:

  • Java内存模型即Java Memory Model,简称JMM。JMM定义了Java 虚拟机(JVM)在计算机内存(RAM)中的工作方式;
  • JVM是整个计算机虚拟模型,所以JMM是隶属于JVM的。

2.2、背景和痛点:

  1. 由于CPU 和主内存间存在数量级的速率差,引入了多级高速缓存的传统硬件内存架构,但在多线程并发场景会带来了缓存一致性问题(引发可见性问题)
  2. 为了提高效率,编译器会对代码指令重排序(引发有序性问题)处理器优化会对代码乱序执行(引发原子性问题)。
  • 编译器优化的重排序(编译器重排序):编译器在不改变单线程程序语义放入前提下,可以重新安排语句的执行顺序。
  • 指令级并行的重排序(处理器重排序):现代处理器采用了指令级并行技术来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。
  • 内存系统的重排序(处理器重排序):由于处理器使用缓存和读写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。

2.3、JMM如何解决:

内存模型解决并发问题主要采用两种方式:限制处理器优化(重排序)使用内存屏障

  • JMM定义了八种操作:lock, unlock, read, load,use,assign, store, write;
  • 内存屏障、happens-before、锁机制、CAS等。

3、支撑 JMM 的基础原理

3.1、Java 线程与主内存的关系

Java 内存模型是一种规范,定义了很多东西:

  • 所有的变量都存储在主内存(Main Memory)中。
  • 每个线程都有一个私有的本地内存(Local Memory),本地内存中存储了该线程以读/写共享变量的拷贝副本。
  • 线程对变量的所有操作都必须在本地内存中进行,而不能直接读写主内存。
  • 不同的线程之间无法直接访问对方本地内存中的变量。
    在这里插入图片描述

3.2、JMM 定义了八种操作

为了更好的控制主内存和本地内存的交互,Java 内存模型定义了八种操作来实现

  • lock(锁定):作用于主内存的变量,把一个变量标识为一条线程独占的状态。
  • unclock(解锁):作用于主内存的变量,把一个处于锁定的状态释放出来。
  • read(读取):作用于主内存的变量,把一个变量的值从主内存传输到线程的工作内存中
  • load(载入):作用于工作内存的变量,把read操作从主内存 得到的变量值放入工作内存的变量副本中。
  • use(使用):作用于工作内存的变量,把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用到变量的值的字节码指令时将会执行这个操作。
  • assign(赋值):作用于工作内存的变量,把一个从执行引擎接收到的值 赋值给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
  • store(存储):作用于工作内存的变量,把工作内存中的一个变量的值传递到主内存,以便write操作使用。
  • write(写入):作用于主内存的变量,把store操作从工作内存中得到的变量的值放入主内存的变量中。

3.3、数据依赖性

如果两个操作访问同一个变量,其中一个为写操作,此时这两个操作之间存在数据依赖性。
编译器和处理器不会改变存在数据依赖性关系的两个操作的执行顺序,即不会重排序。

3.4、as-if-serial语义

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

3.5、内存屏障(Memory Barrier )

通过内存屏障可以禁止特定类型处理器的重排序,从而让程序按我们预想的流程去执行。内存屏障,又称内存栅栏,是一个CPU指令,基本上它是一条这样的指令:

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

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

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

这和java有什么关系?上面java内存模型中讲到的volatile是基于Memory Barrier实现的

如果一个变量是volatile修饰的,JMM会在写入这个字段之后插进一个Write-Barrier指令,并在读这个字段之前插入一个Read-Barrier指令。这意味着,如果写入一个volatile变量,就可以保证:

  1. 一个线程写入变量a后,任何线程访问该变量都会拿到最新值。
  2. 在写入变量a之前的写入操作,其更新的数据对于其他线程也是可见的。因为Memory Barrier会刷出cache中的所有先前的写入。

3.6、happens-before 原则

  • 程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作
  • 锁定规则:一个unLock操作先行发生于后面对同一个锁额lock操作
  • volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作
  • 传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C
  • 线程启动规则:Thread对象的start()方法先行发生于此线程的每个一个动作
  • 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生
  • 线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行
  • 对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始

3.7、volatile的原理和实现机制

观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令,lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:

  1. 它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;
  2. 它会强制将对缓存的修改操作立即写入主存;
  3. 如果是写操作,它会导致其他CPU中对应的缓存行无效。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值