Java并发(二):Java内存模型

一、硬件内存架构

一个现代计算机通常由两个或者多个CPU。其中一些CPU还有多核。每个CPU在某一时刻运行一个线程是没有问题的。如果你的Java程序是多线程的,在你的Java程序中每个CPU上一个线程可能同时(并发)执行。

当一个CPU需要读取主存时,它会将主存的部分读到CPU缓存中。它甚至可能将缓存中的部分内容读到它的内部寄存器中,然后在寄存器中执行操作。

当CPU需要将结果写回到主存中去时,它会将内部寄存器的值刷新到缓存中,然后在某个时间点将值刷新回主存。

二、并发编程的问题

并发编程,为了保证数据的安全,需要满足以下三个特性:

原子性:在一个操作中就是cpu不可以在中途暂停然后再调度,既不被中断操作,要不执行完成,要不就不执行。(处理器优化)

  原子性问题:线程在执行一个读改写操作时,在执行读改之后,时间片耗完,就会被要求放弃CPU,并等待重新调度。此时另一个线程对同一个变量执行读改写操作就会出现问题。这种情况下,读改写就不是一个原子操作。

i = 0;      // 基本数据类型的变量和赋值操作都是原子性操作
j = i ;     // 包含了两个操作:读取i,将i值赋值给j 
i++;         // 包含了三个操作:读取i值、i + 1 、将+1结果赋值给i
i = j + 1;  // 包含了三个操作:读取j值、j + 1 、将+1结果赋值给i

  在单线程环境下我们可以认为整个步骤都是原子性操作。但是在多线程环境下则不同,Java只保证了基本数据类型的变量和赋值操作才是原子性的。要想在多线程环境下保证原子性,则可以通过锁、synchronized来确保。

可见性:当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。(缓存一致性问题)

有序性:程序执行的顺序按照代码的先后顺序执行。(指令重排)

内存模型通过限制处理器优化和使用内存屏障,来保证共享内存的正确性(可见性、有序性、原子性)。

Java内存模型(Java Memory Model ,JMM)就是一种符合内存模型规范的,屏蔽了各种硬件和操作系统的访问差异的,保证了Java程序在各种平台下对内存的访问都能保证效果一致的机制及规范。

JMM还通过volatilesynchronizedfinalconcurren包等实现原子性、有序性、可见性。

三、Java内存模型(JMM)

共享变量:堆内存在线程之间共享,存储在堆内存中所有实例域、静态域和数组元素共享变量

  (局部变量,方法定义参数、异常处理器参数不会在线程之间共享,不会有内存可见性问题,不受内存模型的影响)

JMM定义了线程和主内存之间的抽象关系:

  1)线程之间的共享变量存储在主内存(main memory)中

  2)每个线程都有一个私有的本地内存(local memory),本地内存中存储了该线程用以读/写共享变量的副本

  3)本地内存是JMM的一个抽象概念,并不真实存在。它涵盖了缓存,写缓冲区,寄存器以及其他的硬件和编译器优化

线程A与线程B通信:

  1)线程A把本地内存A中更新过的共享变量刷新到主内存中去

  2)线程B到主内存中去读取线程A之前已更新过的共享变量

JMM通过控制主内存与每个线程的本地内存之间的交互,提供内存可见性保证

JMM的设计

1)常见的处理器内存模型比JMM要弱,java编译器在生成字节码时,会在执行指令序列的适当位置插入内存屏障来限制处理器的重排序。

2)由于各种处理器内存模型的强弱并不相同,为了在不同的处理器平台向程序员展示一个一致的内存模型,JMM在不同的处理器中需要插入的内存屏障的数量和种类也不相同。

程序员希望:强内存模型编程,易于理解,易于编程

编译器和处理器希望:弱内存模型,内存模型对它们的束缚越少越好,以提高性能

JMM时的核心目标就是找到一个好的平衡点:一方面要为程序员提供足够强的内存可见性保证;另一方面,对编译器和处理器的限制要尽可能的放松。

JMM把happens- before要求禁止的重排序分为了下面两类:

1)会改变程序执行结果的重排序,JMM要求编译器和处理器必须禁止这种重排序。

2)不会改变程序执行结果的重排序,JMM对编译器和处理器不作要求(JMM允许这种重排序)。

  只要不改变程序的执行结果(指的是单线程程序和正确同步的多线程程序),编译器和处理器怎么优化都行。

  比如,如果编译器经过细致的分析后,认定一个锁只会被单个线程访问,那么这个锁可以被消除。

  再比如,如果编译器经过细致的分析后,认定一个volatile变量仅仅只会被单个线程访问,那么编译器可以把这个volatile变量当作一个普通变量来对待。

  这些优化既不会改变程序的执行结果,又能提高程序的执行效率。

四、顺序一致性内存模型

顺序一致性内存模型是一个被计算机科学家理想化了的理论参考模型,它为程序员提供了极强的内存可见性保证(JMM没有顺序一致性内存模型保证)

特性:

  • 一个线程中的所有操作必须按照程序的顺序来执行。
  • (不管程序是否同步)所有线程都只能看到一个单一的操作执行顺序。在顺序一致性内存模型中,每个操作都必须原子执行且立刻对所有线程可见。

视图信息:

1.顺序一致性模型有一个单一的全局内存

2.在任意时间点最多只能有一个线程可以连接到内存

3.每一个线程必须按程序的顺序来执行内存读/写操作

举例:

线程A:A1->A2->A3  线程B:B1->B2->B3  并发执行

正确同步:

两个线程没有做同步:

可以看出:

1.每个线程内部执行顺序 都是按照程序的顺序来执行

2.所有线程都只能看到一个一致的整体执行顺序(原因:顺序一致性内存模型中的每个操作必须立即对任意线程可见)

顺序一致性模型与JMM区别:

  顺序一致性模型保证单线程内的操作会按程序的顺序执行,JMM不保证单线程内的操作会按程序的顺序执行(遵守as-if-serial语义)

  顺序一致性模型保证所有线程只能看到一致的操作执行顺序,而JMM不保证所有线程能看到一致的操作执行顺序

JMM在具体实现上的基本方针:在不改变(正确同步的)程序执行结果的前提下,尽可能的为编译器和处理器的优化打开方便之门。 

正确同步,JMM保证程序的执行结果将与该程序在顺序一致性模型中的执行结果相同(但不保证执行顺序)

 

假设A线程执行writer()方法后,B线程执行reader()方法

五、处理器内存模型

如果完全按照顺序一致性模型来实现,那么很多的处理器和编译器优化都要被禁止,这对执行性能将会有很大的影响。

根据对不同类型读/写操作组合的执行顺序的放松,可以把常见处理器的内存模型划分为下面几种类型:

  1. 放松程序中写-读操作的顺序,由此产生了total store ordering内存模型(简称为TSO)。
  2. 在前面1的基础上,继续放松程序中写-写操作的顺序,由此产生了partial store order 内存模型(简称为PSO)。
  3. 在前面1和2的基础上,继续放松程序中读-写和读-读操作的顺序,由此产生了relaxed memory order内存模型(简称为RMO)和PowerPC内存模型。

注意,这里处理器对读/写操作的放松,是以两个操作之间不存在数据依赖性为前提的(因为处理器要遵守as-if-serial语义,处理器不会对存在数据依赖性的两个内存操作做重排序)。

从上到下,模型由强变弱。越是追求性能的处理器,内存模型设计的会越弱。因为这些处理器希望内存模型对它们的束缚越少越好,这样它们就可以做尽可能多的优化来提高性能。

JMM,处理器内存模型,顺序一致性内存模型之间的关系

JMM是一个语言级的内存模型,处理器内存模型是硬件级的内存模型,顺序一致性内存模型是一个理论参考模型。

语言内存模型,处理器内存模型和顺序一致性内存模型的强弱对比示意图:

内存模型越强,越容易保证内存可见性,易编程性就越好。但是重排序就会越少,执行效率就越低。

 

重排序 :Java并发(三):重排序

happens-before:Java并发(四):happens-before

volatile:Java并发(六):volatile的实现原理

Final:Java并发(十九):final实现原理

 

 

 参考资料:

《成神之路-基础篇》JVM——Java内存模型 

细说Java多线程之内存可见性

Java内存模型FAQ

深入理解Java内存模型

 

posted @ 2018-10-18 17:56  那股泥石流  阅读(1996)  评论(0编辑  收藏  举报