在学习Java并发编程中,了解Java内存模型对于我们去理解Java多线程编程是非常有帮助的,本文将对JMM进行一个大体介绍,让我们对JMM有一个大体的轮廓。
硬件的内存模型
为了提高效率,充分利用计算机的能力,多任务处理已经成为现代计算机的必备功能。与软件级的并发类似,硬件级的并发也会出现并发访问问题。例如现在的计算机一般是多核计算的,而共享内存区域却只有一块,多处理器与内存之间的数据共享就会出现并发问题。
由于存储器和存储设备之间的速度差了很多数量级,所以为了提高效率必须加入一层读写速度尽可能接近处理器运算速度的高速缓存来作为内存与处理器之间的缓冲:将运算需要的数据复制到缓存中,充分利用处理器计算能力,当运算结束后再从缓存同步回内存中。虽然基于高速缓存的存储交互很好的解决了处理器与内存之间的矛盾,却也带来更高的复杂度:缓存一致性问题。在多处理器系统中,每个处理器都有自己的高速缓存,它们也共享同一个主内存,当多个处理器的运算任务都涉及同一块主内存区域时,将可能导致各自的数据不一致。
下图是硬件的通用内存模型
Java内存模型
并发编程模型中的两个问题
-
线程之间如何通信
通信是指线程之间以何种机制来交换信息。在命令式编程中,线程之间的通信机制有两种:共享内存和消息传递。
-
在共享内存的并发模型中,线程之间共享程序的公共状态,通过读-写内存中的公共状态进行隐式通信。
-
在消息传递的并发模型里,线程之间没有公共状态,线程之间必须通过发送消息来显式通信。
-
-
线程之间如何同步
同步是指程序中用于控制不同线程间操作发生相对顺序的机制。在共享内存模型中,同步是显式进行的——程序员必须显示指定某个方法或者某段代码需要在线程之间互斥执行。
Java并发采用的是共享内存内存模型,Java线程之间的通信是隐式进行的,整个通信过程对程序员完全透明。
Java内存模型
Java内存模型(Java Memory Model, JMM)是一种由Java语言规范规定的抽象内存模型,旨在屏蔽掉各种硬件和操作系统在内存访问上的差异,实现让Java程序在各种平台下都能达到一致的内存访问效果。
在Java中,所有的实力域、静态域和数组元素都存储在堆内存中,堆内存中的数据是多线程共享的共享变量。局部变量、方法定义参数、异常处理器参数不会在线程之间共享,它们不会存在内存可见性问题,不受内存模型的影响。
Java线程之间的通信由Java内存模型来完成,JMM决定一个线程对共享变量的写入何时对另一个线程可见。JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存中,每条线程都有一个私有的工作内存,工作内存中存储了被该线程使用到的主内存中共享变量的副本,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存中的变量,不同的线程之间也不能直接访问对方的工作内存中的变量,线程之间变量值的传递需要主内存来完成。下图是线程,工作内存,主内存三者交互图:
与前面的硬件模型类比:
-
线程类比于处理器,是执行单元
-
工作内存类比于高速缓存
-
主内存类比于硬件模型中的主内存(JMM中的主内存只是虚拟机的一部分)
主内存与工作内存之间的交互操作
Java内存模型定义了8种用于完成主内存和工作内存之间交互协议的操作,即一个变量如何从主内存拷贝到工作内存、如何从工作内存同步回主内存的实现细节。虚拟机实现时必须保证8种操作都是原子的、不可再分的:
-
lock(锁定): 作用于主内存的变量,把一个变量标识为线程独占的状态,即某一时刻只能有一条线程拥有此变量,其他线程只能等待
-
unlock(解锁): 作用于主内存的变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定
-
read(读取): 作用于主内存的变量,把一个变量的值从主内存传送到线程的工作内存中,以便随后的load操作使用
-
load(载入): 作用于工作内存的变量,把read操作从主内存得到的变量的值放入工作内存的变量副本中
-
use(使用): 作用于工作内存的变量,把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用到变量的值的字节码指令时会执行这个操作
-
assign(赋值): 作用工作内存的变量,把一个从执行引擎收到的值赋给工作内存的变量,没当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作
-
store(存储): 作用于工作内存的变量,把工作内存中一个变量的值传送到主内存中,以便随后的write操作使用
-
write(写入): 作用于主内存的变量,把store操作从工作内存中得到的变量的值放入到主内存的变量中
相应的这8个操作必须遵守以下规则:
-
不允许read和load、store和write操作之一单独出现
-
不允许线程丢弃最近德assign操作,即变量在工作内存中改变了之后必须把变化同步到主内存
-
没有发生assign操作的变量不能同步到主内存
-
对一个变量实时use、store操作之前,必须先执行过assign和load操作,即一个新的变量只能由主内存创建,不允许在工作内存中直接使用一个未被初始化的变量(初始化: load和assign)
-
一个变量在同一时刻只允许一条线程对其进行lock操作,但lock操作可以被同一条线程重复执行多次,多次执行lock后,只有相同次数的unlock操作变量才会被解锁
-
如果对一个变量执行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前,需要重新执行load或assign操作初始化变量的值
-
如果一个变量事先没有被lock操作锁定,那就不允许对它进行unlock操作,也不允许去unlock一个被其他线程锁定的变量(lock和unlock对应于同一条线程)
-
对一个变量执行unlock操作之前,必须先把此变量同步回主内存(执行store、write)
并发的三个特性
Java内存模型是围绕着在并发过程中如何处理原子性,可见性和有序性3个特征建立的,下面简单介绍一下这三个特性。
-
原子性: 一个操作是不可分割的整体,要么都发生,要么都不发生。Java内存模型直接保证的原子性变量操作包括read、load、assign、use、store和write。我们可以认为基本数据类型的访问读写是具备原子性的(long和double的非原子协定例外)。Java内存模型还提供了lock和unlock操作来满足更大范围的原子性保证,lock和unlock操作没有直接开放给程序员使用,但是提供了更高层次的字节码指令monitorenter和monitorexit隐式的表达这两个操作,反映到Java代码中就是同步块——synchronized关键字,因此synchronized块之间的操作具有原子性。
-
可见性: 指当一个线程修改了共享变量的值,其他线程能够立即得知这个修改。Java内存模型通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值这种依赖主内存作为传播媒介的方式来实现可见性的,无论是普通变量还是volatile变量都如此。普通变量和volatile的区别在于:volatile保证新值能够立即同步到主内存,每次使用前立即从主内存刷新。Java语言中,volatile、synchronized、final都能保证可见性。
-
有序性: 在本线程内观察,所有操作都是有序的(线程内串行执行);在一个线程中观察另一个线程,所有操作都是无序的(指令重排序和内存与主内存之间的同步延迟)。Java语言中,volatile、synchronized能保证线程之间操作的有序性。
总结:synchronized可以保证原子性,可见性和有序性;volatile可以保证可见性和有序性,无法保证原子性(但是可以保证long或double类型的变量的原子性);final可以保证可见性。总之,合理使用并发机制是提高并发能力的关键因素。
long和double的非原子协定
前面说过,基本数据类型的访问读写是具备原子性的,但是long和double除外。JMM要求lock、unlock、read、load、assign、use、store、write这8个操作都具有原子性,对于64位的数据类型(long和double),JMM特别定义了一条相对宽松的规定:允许虚拟机将没有被volatile修饰的64位数据的读写操作划分为两次32位的操作来进行,即不保证load、store、read和write的原子性,这就是long和double的非原子性协定。
如果多个线程共享一个并未声明为volatile的long或double类型的变量,并且同时对他们进行读取和修改操作,那么有些线程可能只会读到一个半值(高32位或低32位)。但是目前各种平台下的商用虚拟机几乎都将64位的数据读写操作作为原子操作来对待,所以我们不必将其专门声明为volatile。
重排序
重排序是编译器和处理器为了提高程序执行性能而做的一些指令序列优化,主要分为三种类型:
-
编译器优化重排序: 编译器在不改变单线程语义的前提下,可以重新安排语句的执行序列。
-
指令级并行重排序: 现在的处理器采用了指令集并行来讲多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。
-
内存系统的重排序: 处理器使用缓存和读/写缓冲区,使得加载和存储操作看上去是在乱序执行。
从源码到指令序列会经历如下3种重排序:
上图中1属于编译器重排序,2和3属于处理器重排序,重排序会导致出现内存可见性问题。对于编译器,JMM的编译器重排序规则会禁止部分重排序。对于处理器(JVM),JMM规则要求在Java编译器在生成指令序列时,插入特定的内存屏障指令来消除特定类型的处理器重排序(volatile的语义之一就是禁止指令重排序,所以可以解决可见性问题)。
线程的浪漫生活
参考
-
《Java并发编程的艺术》
-
《深入理解Java虚拟机》
-
《Java并发编程实战》
-
https://docs.oracle.com/javase/specs/index.html