Java内存模型(JMM)

JMM定义

Java内存模型(Java Memory Model,简称JMM),JMM为Java虚拟机规范中定义的虚拟模型,用来屏蔽各种硬件和操作系统的内存访问差异,主要目标是定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量这样底层细节。此处的变量与Java编程时所说的变量不一样,指包括了实例字段、静态字段和构成数组对象的元素,但是不包括局部变量与方法参数,后者是线程私有的,不会被共享,不存在竞争问题。
Java内存模型中规定:

  • 线程对变量的所有操作(读取、赋值)都必须在工作内存中进行,而不能直接读写主内存中的变量
  • 不同线程之间无法直接访问对方工作内存中的变量,线程间变量值的传递均需要在主内存来完成
    线程、工作内存、主内存交互关系
    线程的工作内存中保存了被该线程使用到的变量的主内存副本拷贝(不会拷贝整个对象,只会拷贝线程访问到的内容)

内存间交互操作

主内存和工作内存的交互协议,Java内存模型中定义了8中操作,虚拟机必须保证每种操作都是原子的、不可再分的(即最小单元)(对于double、long类型,load、store、read和write在某些平台允许例外)
JMM八种原子性操作
上面的八种操作具有一定的执行规则:

  • 不允许 read 和 load、 store 和 write 操作之一单独出现, 即不允许一个变量从主内存读取了但工作内存不接受, 或者从工作内存发起回写了但主内存不接受的情况出现。(必须先执行read再执行load,先执行store在执行write,不必保证连续执行
  • 不允许一个线程丢弃它的最近的 assign 操作, 即变量在工作内存中改变了之后必须把该变化同步回主内存。(assign–>store–>write:每次更新完成立刻写会主内存,保证其他线程可见
  • 不允许一个线程无原因地(没有发生过任何 assign 操作)把数据从线程的工作内存中同步回主内存中。
  • 一个新的变量只能在主内存中 “ 诞生 “,不允许在工作内存中直接使用一个未被初始化 (load 或 assign) 的变量,对一个变量实施use、store操作之前,必须先执行过了load和assign操作。(read–>load–>use:每次使用都重新加载
  • 一个变量在同一时刻只允许一条线程对其进行lock操作,但lock操作可以被同一条线程执行多次,只有执行相同次数的unlock,变量才会被解锁。
  • 如果对变量执行lock操作,将会清空工作内存中该变量的值,在执行引擎使用这个变量前,重新执行 load或assign操作初始化该变量的值。(保证了变量的线程同步)
  • 如果没有lock,就不允许unlock操作,同时也不允许unlock其他线程锁定的变量。
  • 对变量执行unlock之前,必须把此变量同步回主内存中(store、write操作)。

对于long和double类型的特殊规则

Java对于long和double在模型中定义了比较宽松的规定:允许虚拟机将没有被volatile修饰的64位数据的读写分为两次操作进行,即允许虚拟机实现选择可以不保证64位数据类型的load、store、read、write这4个操作的原子性,即long和double的非原子性协定。
多线程情况下,可能存在读到“半个变量”的情况。但是在实际的商用虚拟机中,几乎都选择把64位的读写操作作为原子操作来对待,所以一般情况下不用将long、double设置为volatile变量。

原子性、可见性、有序性

JMM围绕着在并发过程中如何处理可见性、原子性、有序性这三个特性而建立的模型。

原子性(Atomicity)

什么是原子性呢?就是这个操作要么全部做完,要么全部不做,不允许做一半。举例说明:

a = a + 1;

结合线程、工作内存、主内存交互关系图,我们知道这段代码会执行以下三个步骤:

  1. 将a的值从主存拿到线程的工作内存中;
  2. 在工作内存中执行 +1 操作得到新的结果;
  3. 将工作内存中新的结果写回主存。

这三个操作分别具有原子性,CPU完全可能在执行完其中的某个步骤后去执行其他的内容(比如线程A执行完步骤2,线程B也执行了这段代码且执行完成,A再执行步骤3,会覆盖掉线程B的结果),这就可能导致多线程并发情况下的安全问题。如果没有get到原子性的重要性,下面看个接地气的生活“实例”:
街边买个煎饼,扫码付钱10元,包含了两个步骤:你的账户扣10元,店家账户增加10元,如果整个操作不满足原子性,你的账户扣了钱但店家没有收到,那店家能放你走么?如果满足原子性,你的账户扣了钱,店家账户也收到了钱,欢迎下次光临。
JMM直接保证的原子性变量操作包括read、load、assign、use、store和write,我们可以大致认为基本数据类型的访问读写是具备原子性的(long和double具有非原子性协定)。但在我们日常开发中,我们需要大范围的原子性保证(例如整个业务方法),Java还提供了lock、unlock,这两个操作并没有直接交给用户使用,利用字节码指令monitorenter、monitorexit来隐式使用这两个操作,这两个字节码反映到Java中就是同步块synchronized关键字,即synchronized可以保证其修饰的代码块的原子性。

可见性

可见性是指当一个线程修改了共享变量的值,其他线程能够立即得知这个修改。在线程、工作内存、主内存交互关系一图中,我们可以知道Java内存模型是通过在工作内存修改值后刷新到主存,读取时从主存刷新到工作内存这种方式来实现线程间的通信,无论是volatile关键词修饰的变量还是普通变量都是采用的这种方式,但volatile变量和普通变量的区别是:volatile采用内存屏障保证了变量的可见性(内存屏障(Memory Barrier)),普通变量无法保证可见性。synchronized和final也可以保证可见性,synchronized由“对变量执行unlock之前,必须把此变量同步回主内存中(store、write操作)”保证;final的可见性指:被final修饰的字段在构造器中初始化完成,且构造器没有把“this”引用传递出去(this引用逃逸?这个不太懂),那在其他线程中就可以看到final字段的值。

有序性

Java程序天然的有序性可以总结为:如果在本线程内观察,所有的操作均是有序的(线程内表现为串行的语义 As-If-Serial);如果在一个线程内观察另一个线程,所有的操作都是无序的(指令重排序、工作内存与主存同步延迟)。

参考文章

文中部分内容摘自《深入理解Java虚拟机》一书,这本书讲得很好,每次看都有不同的体会。文章作为个人笔记、总结,如有错误、不合适的内容,留言指正。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值