并发(JMM综述)

JMM综述

一,内存模型产生背景

由于计算机的存储设备与处理器的运算速度有几个数量级的差距,为了避免处理器等待缓慢的内存读写操作完成,现代计算机系统通过加入一层读写速度尽可能无限接近处理器运算速度的高速缓存

CPU高速缓存

缓存作为内存和cpu之间的缓冲区,将运算需要用到的数据放入到高速缓存中,让运算能快速运行,运算结束后在从缓存同步回内存之中

类似redis缓存和数据库一样,引入了高速缓存虽然解决了处理器和内存速度的差异,但是却带来一个新的问题-----缓存一致性问题

在多核处理器的系统中,每个处理器都有自己的高速缓存,他们共享同一内存,当多个处理器的运算任务都涉及同一块内存区域时,可能会出现缓存数据不一致的问题,需要使用缓存一致性协议来维护缓存的一致性

缓存一致性

二,内存模型概念

java虚拟机提供java内存模型(JMM)来屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的内存访问效果,JMM提出目标在于,定义程序中各个变量的访问规则,,即在虚拟机中将变量放入内存和从内存中取出变量的过程。

2.1JMM组成部分

  • 共享内存(主内存):存放共享变量
  • 工作内存:是每个线程中的一份私有内存,线程的工作内存不能相互访问,只能访问到共享内存,工作内存中,存放的是共享内存中共享变量的一份拷贝

Javaå†å­˜æ¨¡åž‹æŠ½è±¡ç¤ºæ„å›¾

2.2JVM内存操作的并发问题

2.3内存交互操作流程

三,JMM深入

3.1原子性,可见性和有序性

原子性

一个或者多个操作,要么全部执行并且执行过程中不会被其他因素中断,要么全都不执行即使在多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程所干扰。

可见性

多个线程访问一个共享变量时,一个线程修改了该贡献变量的值,其他线程能立即看到最新被修改后的值,即线程A修改了共享变量值后会立马写回到主内存中,其他线程读取主内存中共享变量的最新值来贡献本地工作内存中的变量值

有序性

有序性规则表现在以下两种场景: 线程内和线程间

  • 线程内 从某个线程的角度看方法的执行,指令会按照一种叫“串行”(as-if-serial)的方式执行,此种方式已经应用于顺序编程语言。
  • 线程间 这个线程“观察”到其他线程并发地执行非同步的代码时,由于指令重排序优化,任何代码都有可能交叉执行。唯一起作用的约束是:对于同步方法,同步块(synchronized关键字修饰)以及volatile字段的操作仍维持相对有序。

3.2happens-before

happens-before规则是用来描述两个操作的可见性的,如果A happens-before B,那么A的结果对B可见

单线程下的 happens-before 字节码的先后顺序天然包含happens-before关系:因为单线程内共享一份工作内存,不存在数据一致性的问题。 在程序控制流路径中靠前的字节码 happens-before 靠后的字节码,即靠前的字节码执行完之后操作结果对靠后的字节码可见。然而,这并不意味着前者一定在后者之前执行。实际上,如果后者不依赖前者的运行结果,那么它们可能会被重排序。

多线程下的如果没有实现 happens-before 多线程由于每个线程有共享变量的副本,如果没有对共享变量做同步处理,线程1更新执行操作A共享变量的值之后,线程2开始执行操作B,此时操作A产生的结果对操作B不一定可见。

所以,在JMM中,如果想要实现一个操作的结果对另一个操作可见,那么这两个操作之间必须存在happens-before关系,无论是否在一个线程之内

JMM实现了以下支持happens-before的操作:

  • **程序顺序规则:**一个线程中的每个操作,happens-before于该线程的中任意后序操作
  • **监视器锁规则:**对一个锁的解锁,happens-before随后对一个锁的加锁(解锁后的操作结果对下一次加锁前可见)
  • **volatile变量规则:**对一个volatile变量的写happens-before对一个volatile变量的读(写入一个volatile变量的结果对下一次volatile变量读操作可见)
  • **传递性:**如果A happens-before B,B happens-before C那么A happens-before C
  • **线程终止法则:**线程中的任何动作都happens-before与其他线程检测到这个线程已经终结,或者从Thread.join方法调用中成功返回,或者 Thread.isAlive 方法返回false。
  • **中断法则法则:**一个线程调用另一个线程的 interrupt 方法 happens-before 于被中断线程发现中断(通过抛出InterruptedException, 或者调用 isInterrupted 方法和 interrupted 方法)
  • **终结法则:**一个对象的构造函数的结束 happens-before 于这个对象 finalizer 开始。
  • 线程启动规则: Thread对象的start()方法 happens-before 此线程的每个一个动作。

JMM为程序提供了简单的模型(规则),底层是JMM帮我们程序去实现对于某些类型重排序的禁止

img

3.3内存屏障

Java中如何保证底层操作的有序性和可见性?可以通过内存屏障。这是JMM中另外一个禁止指令重排序的方法

内存屏障是被插入两个CPU指令之间的一种指令,用来禁止处理器指令发生重排序(像屏障一样),从而保障有序性的。另外,为了达到屏障的效果,它也会使处理器写入、读取值之前,将主内存的值写入高速缓存,清空无效队列(即把失效(产生数据不一致的)的值清除),从而保障可见性

常见的4中屏障

  • LoadLoad屏障: 对于这样的语句 Load1; LoadLoad; Load2,在Load2及后续读取操作要读取的数据被访问前,保证Load1要读取的数据被读取完毕。(Load1和Load2不能发生指令重排序)
  • StoreStore屏障: 对于这样的语句 Store1; StoreStore; Store2,在Store2及后续写入操作执行前,保证Store1的写入操作对其它处理器可见。(Store1和Store2不能发生重排序
  • LoadStore屏障: 对于这样的语句Load1; LoadStore; Store2,在Store2及后续写入操作被执行前,保证Load1要读取的数据被读取完毕。(Load1和store1不能发生重排序
  • StoreLoad屏障: 对于这样的语句Store1; StoreLoad; Load2,在Load2及后续所有读取操作执行前,保证Store1的写入对所有处理器可见。它的开销是四种屏障中最大的(冲刷写缓冲器,清空无效化队列)。在大多数处理器的实现中,这个屏障是个万能屏障,兼具其它三种内存屏障的功能。(Store和Load不能发生重排序)

3.3重排序

指令重排序是指编译器和处理器为了优化程序性能而对指令序列进行重新排序的一种手段

as-if-serial:

不管怎么重排序,单线程程序执行结果不能被改变

  • 编译器优化的重排序。编译器在不改变单线程程序语义(as-if-serial )的前提下,可以重新安排语句的执行顺序。
  • 指令级并行的重排序。现代处理器采用了指令级并行技术(Instruction Level Parallelism,ILP)来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对机器指令的执行顺序。
  • 内存系统的重排序。由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。 从Java源代码到最终实际执行的指令序列,会分别经历下面3种重排序

img

JMM对程序采取了不同的策略:

  • 对于会改变程序执行结果的重排序,JMM要求编译器和处理器必须禁止这种重排序
  • 对于不会改变程序执行结果的重排序,JMM对编译器和处理器不做要求(即允许这种重排序)
    • 在单线程程序中,对于存在控制依赖的操作重排序,不会改变执行结果
    • 但是在多线程程序中,对于存在控制依赖的操作重排序,可能会改变程序执行结果

所以:对于重排序可能会导致的多线程程序出现的内存可见性问题

  • 对于编译器,JMM的编译器重排序规则会禁止特定类型的编译器重排序
  • 对于处理器重排序,JMM的处理器重排序规则会要求java编译器在生成指令序列时,插入特定类型的内存屏障指令,通过内存屏障指令来禁止特定类型的处理器重排序

四,volatile

4.1volatile内存语义

  • 当写一个volatile变量时,JMM会把该线程对应的本地内存的共享变量值刷新到主内存中
  • 当读一个volatile变量时,JMM会把该线程对应的本地内存的共享变量值失效,该线程需要从主内存中读取共享变量的值

4.2volatile特性

  1. 实现了可见性,保证了不同线程对一个共享变量操作时的可见性,即一个线程修改了共享变量的值会保证这个新值立马对其他线程可见
  2. 实现了有序性:通过内存屏障禁止了某些类型的指令重排序
  3. 只能保证单条读或者单条写命令的原子性,复合操作(i++)不能保证原子性

4.3volatile如何禁止指令重排序

volatile变量的可见性是通过内存屏障实现的,在java编译器生成的指令序列的适当位置插入内存屏障指令来禁止特定类型的处理器重排序,让程序按我们预想的流程去执行

  • 当第二个操作是volatile写时,不管第一个操作是什么,都不能重排序,确保volatile写之前的操作不会被重排序到volatile写之后
  • 当第一个操作是volatile读时,不管第二个操作是什么,都不能重排序,确保volatile读之后的操作不会被编译器重排序到读之前
  • 当第一个是volatile写,第二个是volatile读时,都不能进行重排序

所以,需要通过在指令序列中插入内存屏障来保证执行顺序

在这里插入图片描述

  • 在每个volatile写操作的后面插入一个StoreLoad屏障。 该屏障除了使volatile写操作不会与之后的读操作重排序外,还会刷新处理器缓存,使volatile变量的写更新对其他线程可见。
  • 在每个volatile读操作的后面插入一个LoadStore屏障该屏障除了禁止了volatile读操作与其之后的任何写操作进行重排序,还会刷新处理器缓存,使其他线程volatile变量的写更新对volatile读操作的线程可见。

volatile型变量å†å­˜å±éšœæ’å¥ç­–ç•¥

五,synchronize

通过 synchronized关键字包住的代码区域,对数据的读写进行控制:

  • 读数据 当线程进入到该区域读取变量信息时,对数据的读取也不能从工作内存读取,只能从内存中读取,保证读到的是最新的值。
  • 写数据 在同步区内对变量的写入操作,在离开同步区时就将当前线程内的数据刷新到内存中,保证更新的数据对其他线程的可见性。

内存语义

  • 当线程释放锁时,JMM会把该线程对应的本地内存中的共享变量刷新到主内存中
  • 当线程获取锁时,JMM会把该线程对应的本地内存置为无效,从而使得被监视器保护的临界区代码必须从主内存读取共享变量

六,final

final成员变量必须在声明的时候初始化或者在构造器中初始化,否则就会报编译错误。 final关键字的可见性是指:被final修饰的字段在声明时或者构造器中,一旦初始化完成,那么在其他线程无须同步就能正确看见final字段的值。这是因为一旦初始化完成,final变量的值立刻回写到主内存。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值