天天看点

JVM03--JMM(JAVA内存模型)1.物理机解决并发的方案2.Java内存模型

目录

1.物理机解决并发的方案

2.Java内存模型

2.1概述

2.2内存间交互操作

2.3happens-before原则

2.3.1概述

2.3.2为什么需要happens-before?

2.3.3有哪些happens-before规则?

1.物理机解决并发的方案

  • 解决的首要问题:CPU运算速度和物理机存储设备之间的存在的巨大速度差异
  • 解决的办法:通过在CPU和内存之间增加一层独写速度尽可能接近处理器运算速度的高速缓存,作为两者之间的缓冲。将CPU运算需要的数据复制到高速缓存中,保证运算能够快速的进行;当CPU运算结束后,再将运算结果从缓存同步回内存中,这样CPU无须再等待缓慢的内存独写(这里的缓慢是相对CPU处理而言的)。
  • 衍生的问题:基于高速缓存的设计方案能够很好地解决处理器与内存间的速度差异矛盾,但同时也引入了一个新的问题“缓存一致性”。在一个多核处理器系统中,每个处理器都有自己的高速缓存,而它们又共享同一主内存。当多个处理器的运算任务涉及到同一块主内存区域时,将可能导致各自的缓存数据不一致。为了解决这一问题,所以要求各个处理器之间遵循一些协议(如MESI:缓存一致性协议),来保证数据读写操作的正确性。
JVM03--JMM(JAVA内存模型)1.物理机解决并发的方案2.Java内存模型

       MESI协议的设计思想:当某个CPU在操作高速缓存中的数据时,如果发现该变量是一个共享变量,也就是说该变量在其他的CPU高速缓存中也存在一个副本,则进行如下操作:

  1. 读取操作,不做任何处理,只是将告诉缓存中的数据读取到寄存器中。
  2. 写入操作,发出信号通知其他CPU将该变量的Cache line置为无效状态,其他CPU在进行该变量读取的时候不得不到主内存中再次获取

2.Java内存模型

2.1概述

       前面对物理机解决并发的方案及其衍生问题进行了一个简单概述,对于JAVA程序来说,并发问题主要解决两个问题:线程通信和线程同步。在多线程为共同完成同一任务而并发执行时,各个线程之间可能存在着数据交互或操作同一份内存数据,并且在执行顺序上存在先后关系,这时我们就需要在多个线程之间加入协调通信机制,来保证程序有序正常执行。线程之间的通信机制主要包括两种:共享内存和消息传递,Java采用的就是共享内存模型。Java多个线程之间通过读-写内存中的“公共状态”来进行通信,这个过程是隐式进行,对程序员来说是完全透明无感知的。为了定义上述“公共状态”的访问规则,抽象出Java内存模型(JMM),JMM决定一个线程对公共状态(共享变量)的写入如何对另一个线程可见。这里的“公共状态”从JVM的角度看指的是线程共享的数据区变量,包括:实例字段、静态字段和构成数组的元素,不包括局部变量和方法参数等,因为它们是线程私有的。

       JAVA内存模型规定了所有共享变量存储在“主内存”中,而每个线程还拥有自己的“工作内存”,线程的工作内存中保存了被当前线程使用到的主内存中共享变量的副本拷贝,线程对共享变量的操作都在工作内存中完成,不能直接独写主内存中的变量。同时,各线程也无法直接访问对方工作内存中的变量,线程之间变量值的传递需要通过主内存来完成。线程、工作内存、主内存三者之间的抽象关系如下图:

JVM03--JMM(JAVA内存模型)1.物理机解决并发的方案2.Java内存模型

2.2内存间交互操作

变量如何从主内存拷贝到线程的工作内存,以及如何从线程工作内存同步回主内存,JMM定义了如下8中操作来实现上述两个过程:

  • lock(锁定):将变量标识为线程独有状态。
  • unlock(解锁):将一个处于锁定状态的变量释放,释放后的变量可以被其他线程使用。
  • read(读取):将一个变量的值从主内存传输到线程的工作内存中,以便后续的load操作使用。
  • load(载入):将完成read操作的变量值放入本地工作内存的变量副本中。
  • use(使用):将线程本地工作内存中的变量值传递给执行引擎,每当虚拟机遇到一个需要使用该变量值的字节码指令时将回执行该步操作。
  • assign(赋值):将一个从执行引擎收到的值赋值给工作内存中的变量,每当虚拟机遇到一个需要给变量赋值的字节码指令时执行该步操作。
  • store(存储):将工作内存中的变量值传送到主内存中,以便后续的write操作使用。
  • write(写入):将完成store操作的变量值写入到主内存的变量中。

同时在Java内存模型中明确规定了要执行这些操作需要满足以下规则:

  • 不允许read和load、store和write的操作单独出现。
  • 不允许一个线程丢弃它的最近assign的操作,即变量在工作内存中改变了之后必须同步到主内存中。
  • 不允许一个线程无原因地(没有发生过任何assign操作)把数据从工作内存同步回主内存中。
  • 一个新的变量只能在主内存中诞生,不允许在工作内存中直接使用一个未被初始化(load或assign)的变量。即就是对一个变量实施use和store操作之前,必须先执行过了assign和load操作。
  • 一个变量在同一时刻只允许一条线程对其进行lock操作,lock和unlock必须成对出现
  • 如果对一个变量执行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前需要重新执行load或assign操作初始化变量的值
  • 如果一个变量事先没有被lock操作锁定,则不允许对它执行unlock操作;也不允许去unlock一个被其他线程锁定的变量。
  • 对一个变量执行unlock操作之前,必须先把此变量同步到主内存中(执行store和write操作)。

2.3happens-before原则

2.3.1概述

上面讨论了Java中多线程中共享变量的可见性问题及产生这种问题的原因。下面我们看一下如何解决这个问题,即当一个多线程共享变量被某个线程修改后,如何让这个修改被需要读取这个变量的线程感知到。为了方便程序员开发,将底层的烦琐细节屏蔽掉,JMM定义了Happens-Before原则。

《JSR-133:Java Memeory Model and Thread Specification》对happens-before关系的定义如下:

  1. 如果一个操作happens-before另一个操作,那么第一个操作的执行结果将对第二个操作可见,而且第一个操作的执行顺序排在第二个操作之前。
  2. 如果两个操作之间存在happens-before关系,并不意味着Java平台的具体实现必须要按照happens-before关系指定的顺序来执行。如果重排序之后的执行结果,与按照happens-before关系来执行的结果一致,那么这种重排序并不认为是非法的。

上面的1)是JMM对程序员的承诺。从程序员的角度来看,如果A happens-before B,那么JMM向程序员保证:A操作的结果将对B可见,且A的执行顺序排在B之前。

上面的2)是JMM对编译器和处理器重排序的约束原则。JMM遵循一个基本的原则:只要不改变程序的执行结果(指的是单线程和正确同步的多线程程序),编译器和处理器怎么进行优化都被允许。因为程序员对于两个操作是否被重排序并不关心,关心的只是程序执行结果不能被改变。

2.3.2为什么需要happens-before?

因为JVM会对代码进行编译优化,会出现指令重排序情况,为了避免编译优化对并发编程安全性的影响,需要happens-before规则定义一些禁止编译优化的场景,保证并发编程的正确性。

2.3.3有哪些happens-before规则?

程序顺序规则:在一个线程内部,按照代码书写顺序,书写在前面的代码happens-before后边的代码。如果多个操作之间有先后依赖关系,则不允许对这些操作进行重排序。

监视器锁规则:在多线程或单线程环境下,对同一个锁来说,一个线程对这个锁解锁后,另一个线程在获取该锁时,能够看到前一个线程的操作结果。同一时刻只能有一个线程执行锁中的操作,所以锁中的操作被重排序外界是不关心的,只要最终结果能被外界感知到就好。除了重排序,剩下影响变量可见性的就是CPU缓存了。在锁被释放时,A线程会把释放锁之前所有的操作结果同步到主内存中,而在获取锁时,B线程会使自己CPU的缓存失效,重新从主内存中读取变量的值。这样,A线程中的操作结果就会被B线程感知到了。

volatile变量规则:对一个volatile变量的写操作,happens-before于后续对这个volatile变量的读操作。

传递性:如果A操作happens-beforeB操作,B操作happens-beforeC操作,则A操作happens-beforeC操作

线程start()规则:如果在线程A中执行线程B.start()方法,那么A线程中线程B.start()方法及其前面的所有操作happens-before 线程B中的所有操作。线程启动规则可以这样去理解:调用start方法时,会将start方法之前所有操作的结果同步到主内存中,新线程创建好后,需要从主内存获取数据。这样在start方法调用之前的所有操作结果对于新创建的线程都是可见的。

线程join()规则:如果在线程A中执行线程B.join()方法,则线程B中的任意操作都happens-before线程A从线程B操作成功返回。假设两个线程A、B。在线程A中调用B.join()方法。则线程A会被挂起,等待B线程运行结束才能恢复执行。当B.join()成功返回时,A线程就知道B线程已经结束了。所以根据本条原则,在B线程中对某个共享变量的修改,对A线程都是可见的。

中断规则:一个线程在另一个线程上调用interrupt,Happens-Before被中断线程检测到interrupt被调用。假设两个线程A和B,A先做了一些操作operationA,然后调用B线程的interrupt方法。当B线程感知到自己的中断标识被设置时(通过抛出InterruptedException,或调用interrupted和isInterrupted),operationA中的操作结果对B都是可见的。

终结器规则:一个对象的构造函数执行结束Happens-Before它的finalize()方法的开始。“结束”和“开始”表明在时间上,一个对象的构造函数必须在它的finalize()方法调用时执行完。根据这条原则,可以确保在对象的finalize方法执行时,该对象的所有field字段值都是可见的。