【Java笔记】理解Java Memory Model+内存可见性+指令重排序

本文详细阐述了Java内存模型、指令重排序的必要性、三种指令重排序类型、数据竞争与顺序一致性模型的区别,以及JMM如何与顺序一致性协同工作,特别强调了Happens-before关系在编程实践中的重要性。
摘要由CSDN通过智能技术生成

一些概念大杂烩

Java Memory Model

JMM描述线程之间如何通过内存来进行交互,其中的概念是抽象的(相对的JVM运行时数据区是具体的),包括:

  • 本地内存:线程私有,包括栈区、程序计数区
  • 主内存:线程共享,包括方法区与堆区

其中,

  • 所有的共享变量都存在主内存中。
  • 每个线程都保存了一份该线程使用到的共享变量的副本,线程堆共享变量的操作都需要在本地内存中。
  • 线程A无法直接访问线程B的工作内存,线程间通信必须经过主内存。若线程A与线程B之间通信包括2个步骤:
    1. 线程A将本地内存A中更新过的共享变量刷新到主内存中去。
    2. 线程B到主内存中去读取线程A之前已经更新过的共享变量。

内存可见性

  • 可见性:线程1读取对象状态,线程2同时修改状态,需确保线程2状态修改后让线程1同步读取到变化

  • 可见性错误:JMM中,线程交互都发生在主内存中,对于变量的修改在自己私有的本地内存中(先修改副本再同步),因此同时读写可能造成共享变量状态不一致的错误。

指令重排序

为什么需要指令重排序

流水线模式 Pipeline

每一个指令都会包含多个步骤,为了提高执行效率,采用流水线(pipeline)模式,使得指令1还没有执行完,就可以开始执行指令2。

中断

流水线技术最害怕中断,恢复中断的代价是比较大的,比如缺页中断需要页置换(FIFO、LRU、LFU

、OPT),把新的目标数据读入主存,而IO时间一般较长,IO期间CPU闲置,就会导致CPU利用率降低。

缺页中断:要访问的页不在主存,需要操作系统将其调入主存后再进行访问。

其实,内存IO期间,CPU可以执行其他指令,提高CPU利用率,这也就是指令重排序的工作

三种指令重排序

  • 编译器优化重排:编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。
  • 指令并行重排:现代处理器采用了指令级并行技术来将多条指令重叠执行。如果不存在数据依赖性(即后一个执行的语句无需依赖前面执行的语句的结果),处理器可以改变语句对应的机器指令的执行顺序。
  • 内存系统重排:由于处理器使用缓存和读写缓存冲区,这使得加载(load)和存储(store)操作看上去可能是在乱序执行,因为三级缓存的存在,导致内存与缓存的数据同步存在时间差

需要注意的是,指令重排序保证串行(单线程)中的语义一致。但多线程场景下,不做同步的话可能存在一些问题,比如数据竞争。

数据竞争与顺序一致性模型

数据竞争

数据竞争说白了就是,一个线程更改变量,一个线程读取变量,读写未同步。

会产生一些不确定性,比如读在写前就读不到最新的值。当然,同步后就没这些问题了,同步的方法包括volatilefinalsynchronized等关键字、Lock

顺序一致性模型

顺序一致性模型是一个理想化的理论参考模型:

  • 一个线程中的所有操作必须按照程序(代码)顺序
  • 不管线程是否同步,每个操作都是原子性,且对所有线程可见

那么如何实现这个理想的模型呢?最容易想到的就是所有指令全部按顺序串行执行,不要考虑什么多线程、重排序优化了。

但这显然不可能,可以看出顺序一致性模型与性能优化是矛盾的。

JMM与顺序一致性

考虑到性能优化,JMM中不能严格意义上保证这两个特性:

  • 一方面,重排序在保证CPU利用率同时,保证不了代码的执行顺序。
  • 另一方面,JMM各个线程修改(堆、方法区中的)共享变量时,需先对各自私有的本地内存中的副本进行更改(因为如果都直接操作共享内存,效率太低,详细的可以了解下CPU三级缓存与内存关系,这里不展开了),再同步至共享内存。更改本地副本时,对其他线程是不可见的。

但是可以达到类似的效果,也就是实际执行结果与顺序一致性模型中的执行结果相同。

JMM同步程序与顺序一致性

一般说到同步,肯定是想到加锁(synchronized、Lock)、信号量、或者同步关键字(volatile、final)等等。对于加锁操作来说,重点就是临界区(锁住的代码区域)内的代码执行过程中,其他线程会阻塞等待,以此来保证线程间的内存可见性。

临界区内的代码会发生指令重排序吗?


此时已经避免了多线程数据竞争的情况,所以用指令重排也不会影响内存可见性之类的(不改变执行结果),同时能提高性能。

因此,JMM同步程序的顺序一致性,我个人理解就是保证了“进入临界区前-临界区-从临界区出来后”的结果与顺序一致性模型中的运行结果相同,但同时允许临界区内的指令重排序来提高性能。

JMM未同步程序与顺序一致性

对于未同步的程序,JMM基本就不保证实际执行结果与顺序一致性模型中的执行结果相同了。主要也是优先保证性能。

happens-before

首先明确一点,happens before不是JVM或者JMM约束计算机的规则或规范,而是给程序员写代码的规范:

程序员只要遵循happens-before规则,那他写的程序就能保证在JMM中具有强的内存可见性。

happens-before关系就是定义(同一线程或不同线程的)两个操作的前后与可见性关系:

  • 操作1 happens-before 操作2,则操作1所有共享内存上的操作对于操作2来说都是可见的
  • 上一条看来,似乎操作1在操作2前执行,但这并不是必须的。如果操作2在操作1前执行(重排序后),结果仍然保持一致性,则JVM可以进行指令重排序

Java中的happens-before关系

  • 程序顺序规则:单个线程中的任一操作,happens-before与该线程中后续任一操作
  • 监视器锁规则:一个锁的解锁,happend-before于随后该锁的加锁
  • volatile变量规则;一个volatile域的写,happend-before于任意对该volatile域的读
  • 线程start:如果线程A执行操作ThreadB.start()启动线程B,那么A线程的ThreadB.start()操作happens-before于线程B中的任意操作
  • 线程join:如果线程A执行操作ThreadB.join()并成功返回,那么线程B中的任意操作happens-before于线程A从ThreadB.join()操作成功返回

Reference

http://concurrent.redspider.group/article/02/7.html
https://cloud.tencent.com/developer/article/1781862

  • 27
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值