《Java虚拟机》之内存模型与线程(上)

一.Java内存模型 JMM(Java Memory Model)的出现是为了屏蔽掉各种硬件和操作系统之间存在的内存访问差异,以期实现Java程序在各种平台上都可以达到一致的内存访问效果。
1.1主内存和工作内存
Java内存模型的只要目标时定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中去除变量这样的底层细节。需要注意一点,这里的变量与Java编程中的变量有所区别。它包括了实例字段,静态字段,和构成数组对象的元素,但是不包括局部变量与方法参数,因为后者是线程私有的,不会被共享。
Java内存模型规定了所有的变量都存储在主内存(Main Memory)中(此处的主内存与介绍物理硬件时的主内存名字一样,两者可以互相类比,但此处仅是虚拟机内存的一部分)。每条线程都有自己的工作内存(Working Memory),线程的工作内存保存了被该线程所使用到的变量的主内存副本拷贝,线程对变量的所有操作(读取,赋值等)都必须在工作内存当中进行,而不能直接读写主内存中的变量。当然了,不同的线程之间也无法直接访问对方工作内存中的变量,线程之间的变量值的传递均需要通过主内存来完成。
《Java虚拟机》之内存模型与线程(上)
文章图片

1.2内存间交互操作
针对一个变量如何从主内存拷贝到工作内存,以及如何从工作内存同步回主内存之类的实现细节,Java内存模型定义了8种操作来实现。值得强调的是,虚拟机在实现时必须保证8种操作每一个都是原子的,不可再分的:

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

对于上述的把一个变量从主内存复制到工作内存,就要顺序的执行read和load操作;如果是把变量从工作内存同步回主内存,就要顺序的执行store和write操作。这两个操作要求的是必须按照顺序执行,而不是连续执行。同时在执行8种基本操作时必须满足下面的规则:
  • 不允许read和load,store和write操作之一单独出现,即不允许一个变量从主内存读取了但是工作内存不接受,或者是从工作内存发起回写但是主内存不接受的情况出现。
  • 不允许一个线程丢弃它的最近的assign操作,即变量在工作内存中改变了之后必须把该变化同步回主内存。
  • 不允许一个线程无原因的(没有发生过任何assign操作)把数据从线程的工作内存同步回主内存中。
  • 一个新的变量只能在主内存中“出生”,不允许在工作内存中直接使用一个未被初始化(load或assign)的变量。换句话说,就是对一个变量进行use,store操作之前必须进行了assign和load操作。
  • 一个变量在同一个时刻只允许一条线程对其进行lock操作,但lock操作可以被同一条线程重复执行多次,多次执行lock后,只有在执行相同次数的unlock操作,变量才会被解锁。
  • 如果对一个变量执行lock操作,那将会清空工作内存中此变量的值,在执行引擎使用这个变量前,需要重新执行load或者是assign操作初始化此变量的值。
  • 如果一个变量事先并没有进行过lock操作,那就不允许对其执行unlock操作,也不允许去unlock一个被其他线程锁定的变量。
  • 对一个变量执行unlock操作之前,必须先把此变量同步回主内存中(即执行store,write操作)。
1.3浅析volatile关键字
关键字volatile可以说是Java虚拟机提供的最轻量级的同步机制。 当有一个变量被定义为volatile之后,它将有两种特性:
(1)可见性
保证了此变量对于所有的线程的可见性,这里的“可见性”指当一条线程修改了这个变量的值,新值对于其他的线程都是立即可见的。
( 2)禁止指令重排序优化
重排序是指编译器和处理器为了优化程序性能而对指令序列进行排序的一种手段。但是重排序也需要遵守一定规则:
1.重排序操作不会对存在数据依赖关系的操作进行重排序。
【《Java虚拟机》之内存模型与线程(上)】比如:a=1; b=a; 这个指令序列,由于第二个操作依赖于第一个操作,所以在编译时和处理器运行时这两个操作不会被重排序。
2.重排序是为了优化性能,但是不管怎么重排序,单线程下程序的执行结果不能被改变
比如:a=1; b=2; c=a+b这三个操作,第一步(a=1)和第二步(b=2)由于不存在数据依赖关系,所以可能会发生重排序,但是c=a+b这个操作是不会被重排序的,因为需要保证最终的结果一定是c=a+b=3。
由于volatile变量只能保证可见性,其具体执行效应为:
1)当写一个volatile变量时,JMM会把该线程对应的本地内存中的变量强制刷新到主内存中去;
2)这个写操作会导致其他线程中的缓存无效。
但是需要注意的是,我们一直在拿volatile和synchronized做对比,仅仅是因为这两个关键字在某些内存语义上有共通之处,volatile并不能完全替代synchronized,它依然是个轻量级锁,在很多场景下,volatile并不能胜任。在不符合以下两条规则的运算场景中,我们任然要通过加锁(使用synchronized或者是java.lang.concurrent中的原子类)来保证原子性:
  • 运算结果并不依赖变量的当前值,或者是能够确保只有单一的线程修改变量的值
  • 变量不需要与其他的状态变量共同参与不变约束
简单总结下,volatile是一种轻量级的同步机制,它主要有两个特性:一是保证共享变量对所有线程的可见性;二是禁止指令重排序优化。同时需要注意的是,volatile对于单个的共享变量的读/写具有原子性,但是像num++这种复合操作,volatile无法保证其原子性,可以使用并发包中的原子操作类,通过循环CAS地方式来保证num++操作的原子性。
最后,回顾JMM对volatile变量定义的特殊规则,假定T表示一个线程,V和W分别表示两个volatile型变量,在进行read,load,use,assign,store,write操作时的具体要求:
  • 只有当线程T对变量V执行的前一个动作是load时,线程T才能对变量V执行use操作;并且,只有当线程T对变量V执行的后一个操作时use时,线程T才能对变量V执行load操作动作。必须连续一起出现(这条规则要求在工作内存中,每次使用V前必须先从主内存刷新最新的值,用于保证能看见其他线程对变量V所做的修改后的值)
  • 只有当线程T对变量V执行的前一个动作时assign操作时,线程T才能对变量V执行store操作;并且,只有当线程T对变量V执行的后一个操作是store操作时,线程T才能对变量V执行assign操作。线程T对变量V的assign操作可以认为是线程T对变量V的store,write动作相关联,必须连续一起出现(这条规则要求在工作内存中,每次修改V后都必须立刻同步回主内存中,由于保证其他线程可以看到自己对变量V所做的修改)
补充一点,除了volatile之外,Java还真有两个关键字可以实现可见性,即synchronized和final。同步块的可见性是由“对一个变量执行unlock操作之前,必须先把此变量同步回主内存中,即执行store,write操作”规则实现的;而final关键字的可见性是指:被final修饰的字段在构造器中一旦初始化完成,并且构造器没有把“this”的引用传递出去,那在其他的线程中就可以看见final字段的值。
1.4先行发生原则
为保证内存模型中所有的有序性,Java提供了一个“先行发生”原则,它是用来判断数据是否存在竞争,线程是否安全的重要依据。先行发生是Java内存模型中定义的两项操作之间的偏序关系:如果操作A先行发生于操作B,其实就是说在发生操作B之前,操作A产生的影响能够被操作B观察到。下面的是Java内存模型中的一些“天然的”先行发生原则:
  • 程序次序规则(Program Order Rule):在一个线程内,按照线程代码顺序,书写在前面的操作先行发生于书写在后面的操作。准确的说,应该是控制流顺序而不是程序代码顺序,还要考虑到分支,循环等因素。
  • 管程锁定规则(Monitor Lock Rule):一个unlock操作先行发生于后面对于同一个锁的lock操作,这里强调的是同一个锁,“后面”指的是时间上的先后顺序。
  • volatile变量规则(Volatile Variable Rule):对于一个volatile变量的写操作先于发生于后面对这个变量的读操作,这里“后面”指的是时间上的先后顺序。
  • 线程启动规则(Thread Start Rule):Thread对象的start()方法先行发生于此线程的每一个动作
  • 线程终止规则(Thread Termination Rule):线程中的所有操作都先行发生于对此线程的终止检测,我们可以通过Thread.join()方法结束,Thread.isAlive()的返回值等手段来检测到线程已经终止执行
  • 线程中断规则(Thread Interruption Rule):对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过Thread.interrupt()方法检测到是否有中断发生
  • 对象终结规则(Finalizer Rule):一个对象的初始化完成(构造函数执行结束)先行发生于它的finalize()方法
  • 传递性(Transitivity):如果操作A先行发生于操作B,操作B先行发生于操作C,那就可以得到操作A先行发生于操作C的结论

    推荐阅读