首页 > 编程语言 > 详细

深入理解Java内存模型JMM(Java Memory Model)

时间:2021-06-06 13:15:51      阅读:23      评论:0      收藏:0      [点我收藏+]

内存模型产生背景

? 在介绍Java内存模型之前,我们先了解一下物理计算机中的并发问题,因为物理机遇到的并发问题与虚拟机中的情况有不少相似之处。

? 由于内存与cpu的缓存运算速度有几个数量级的差距,所以如果将运算数据加载到cpu的缓存中运算提高运算速度,然后结束后再从缓存同步回内存之中。可是这样带来了一个问题 缓存一致性。每个处理器核心都有自己的高速缓存,处理变量时会拷贝一份主存中的变量到各自核心的高速缓存中。当各自核心处理任务时。将会导致各自的缓存数据不一致。为此,要制定一些协议,各个核心在读写时要根据协议进行操作,来维护缓存的一致性。

技术分享图片

 

cpu乱序执行问题

? cpu为了提高运算效率,会对代码进行乱序执行,然后对乱序执行的结果重组。这与代码顺序执行的结果是一样的。可是在一个核的计算任务依赖另一个核计的算任务的中间结果,那么处理器最终得出的结果和我们期望得到的结果可能会大不相同。

 

Java内存模型

? 可以理解Java内存模型就是上述问题的抽象。多个cpu核心抽象成多个线程,主内存抽象成Java的主内存,cpu高速缓存抽象成Java线程的各自内存。 这样我们就好理解了。

主内存:Java内存模型规定了所有变量都存储在主内存( 每条线程都有自己的工作内存)

工作内存:每条线程都有自己的工作内存,又称本地内存,线程的工作内存中保存了该线程使用到的变量在主内存中的共享变量的副本拷贝。

技术分享图片

 

JVM内存操作的并发问题

和前面的物理机介绍类似,下面介绍的Java内存模型的执行处理将围绕解决这2个问题展开:

1.各个线程的工作内存是主内存中的共享变量副本,当多个线程的运算任务都涉及同一个共享变量时,将导致各自的的共享变量副本不一致,数据同步回主内存以谁的副本数据为准? Java内存模型主要通过一系列的数据同步协议、规则来保证数据的一致性。这个后面介绍

2.指令重排问题,编译器为了优化程序性能而对指令进行重新排序执行。多线程环境下,如果线程处理逻辑之间存在依赖关系,有可能因为指令重排序导致运行结果与预期不同

为了解决多线程环境下共享变量的一致性和指令重排问题。Java内存模型定义了三大特性:原子性、可见性、有序性来保证共享变量的一致性。定义先行发生原则(Happens-Before)和内存屏障解决指令重排的有序性问题。在看这两个问题之前我们先熟悉下内存间的交互操作。

 

内存间的交互操作

关于主内存与工作内存之间具体的交互协议,Java内存模型定义了以下8种原子的具体的操作来完成:lock,unlock,read,load,use,assign,store,write。

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

小总结:把一个变量从主内存复制到工作内存,那就要按顺序地执行read和load操作。把一个变量从工作内存同步回主内存,就要按顺序地执行store和write操作。注意这里的按顺序没说要连续。可以在read与load之间、store与write之间插入其它操作。比如,对主内存中的变量a和b的访问,可以按照以下顺序执行:read a -> read b -> load b -> load a。

另外,Java内存模型还定义了执行上述8种操作的基本规则:

  • 不允许read和load、store和write操作之一单独出现,即不允许出现从主内存读取了而工作内存不接受,或者从工作内存回写了但主内存不接受的情况出现。
  • 不允许一个线程丢弃它最近的assign操作,即变量在工作内存变化了必须把该变化同步回主内存。
  • 不允许一个线程无原因地(即未发生过assign操作)把一个变量从工作内存同步回主内存。
  • 一个新的变量必须在主内存中诞生,不允许工作内存中直接使用一个未被初始化(load或assign)过的变量,换句话说就是对一个变量的use和store操作之前必须执行过load和assign操作。
  • 一个变量同一时刻只允许一条线程对其进行lock操作,但lock操作可以被同一个线程执行多次,多次执行lock后,只有执行相同次数的unlock操作,变量才能被解锁。
  • 如果对一个变量执行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前,需要重新执行load或assign操作初始化变量的值。
  • 如果一个变量没有被lock操作锁定,则不允许对其执行unlock操作,也不允许unlock一个其它线程锁定的变量。
  • 对一个变量执行unlock操作之前,必须先把此变量同步回主内存中,即执行store和write操作。

注意,这里的lock和unlock是实现synchronized的基础,Java并没有把lock和unlock操作直接开放给用户使用,但是却提供了两个更高层次的指令来隐式地使用这两个操作,即moniterenter和moniterexit。

 

原子性、可见性、有序性来保证共享变量的一致性

  • 原子性(Atomicity) 即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行 。即使在多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程所干扰。

    由Java内存模型来直接保证的原子性操作包括read、load、user、assign、store、write这些操作,我们可以大致认为基本类型变量的读写是具备原子性的。如果应用需要一个更大范围的原子性,Java内存模型还提供了lock和unlock这两个操作来满足这种需求,尽管不能直接使用这两个操作,但我们可以使用它们更具体的实现synchronized来实现。

  • 可见性(Visibility) 是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值 。

    ? Java内存模型是通过在变更修改后同步回主内存, 依赖主内存作为传递媒介的方式来实现可见性。无论是普通变量还是volatile变量都是如此。普通变量与volatile变量的主要区别是是否会在修改之后立即同步回主内存,以及是否在每次读取前立即从主内存刷新。volatile变量保证了多线程环境下变量的可见性,除了volatile之外,还有两个关键字也可以保证可见性,它们是synchronized和final。

    ? synchronized的可见性是由“对一个变量执行unlock操作之前,必须先把此变量同步回主内存中,即执行store和write操作”这条规则获取的。

    ? final的可见性是指被final修饰的字段在构造器中一旦被初始化完成,那么其它线程中就能看见这个final字段了。

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

 

先行发生原则(Happens-Before)防止指令重排

? 先行发生,是指操作A先行发生于操作B,先行发生的动作的结果一定对于后面的发生的动作可见,这种影响包括修改了共享内存中变量的值、发送了消息、调用了方法等。

  • 程序次序原则

    在一个线程内,按照程序书写的顺序执行,书写在前面的操作先行发生于书写在后面的操作,准确地讲是控制流顺序而不是代码顺序,因为要考虑分支、循环等情况。

  • 监视器锁定原则

    一个unlock操作先行发生于后面对同一个锁的lock操作。

  • volatile原则

    对一个volatile变量的写操作先行发生于后面对该变量的读操作。

  • 线程启动原则

    对线程的start()操作先行发生于线程内的任何操作。

  • 线程终止原则

    线程中的所有操作先行发生于检测到线程终止,可以通过Thread.join()、Thread.isAlive()的返回值检测线程是否已经终止。

  • 线程中断原则

    对线程的interrupt()的调用先行发生于线程的代码中检测到中断事件的发生,可以通过Thread.interrupted()方法检测是否发生中断。

  • 对象终结原则

    一个对象的初始化完成(构造方法执行结束)先行发生于它的finalize()方法的开始。

  • 传递性原则

    如果操作A先行发生于操作B,操作B先行发生于操作C,那么操作A先行发生于操作C。这里说的“先行发生”与“时间上的先发生”没有必然的关系。

总结:

  1. 为实现共享变量的在多个线程的工作内存 数据一致性问题。java内存模型提出了8种原子指令来保证。围绕原子性、可见性、有序性特征建立
  2. 8大操作指令:lock、unlock、read、load、use、assign、store、write。Java内存模型对原子性、可见性、有序性提供了一些实现。
  3. 先行发生的8大原则:程序次序原则、监视器锁定原则、volatile原则、线程启动原则、线程终止原则、线程中断原则、对象终结原则、传递性原则;

深入理解Java内存模型JMM(Java Memory Model)

原文:https://www.cnblogs.com/gaoweiBlog/p/14850947.html

(0)
(0)
   
举报
评论 一句话评论(0
关于我们 - 联系我们 - 留言反馈 - 联系我们:wmxa8@hotmail.com
© 2014 bubuko.com 版权所有
打开技术之扣,分享程序人生!