JMM内存模型、JMM内存间交互操作

主内存与工作内存

在这里插入图片描述

JMM内存间交互操作

       关于主内存与工作内存之间具体的交互协议,即一个变量如何从主内存拷贝到工作内存、如何从工作内存同步回主内存这一类的实现细节,Java内存模型中定义了以下8种操作来完成。Java虚拟机实现时必须保证下面提及的每一种操作都是原子的、不可再分的(对于double和long类型的变量来说, load、store、read和write操作在某些平台上允许有例外)

       针对long和double型变量的特殊规则Java内存模型要求lock、unlock、read、load、assign、use、store、write这八种操作都具有原子性, 但是对于64位的数据类型(long和double),在模型中特别定义了一条宽松的规定:允许虚拟机将没有被volatile修饰的64位数据的读写操作划分为两次32位的操作来进行,即允许虚拟机实现自行选择是否要保证64位数据类型的load、store、read和write这四个操作的原子性,这就是所谓的“long和double的非原子性协定”(Non-Atomic Treatment of double and long Variables)。

       如果有多个线程共享一个并未声明为volatile的long或double类型的变量,并且同时对它们进行读取 和修改操作,那么某些线程可能会读取到一个既不是原值,也不是其他线程修改值的代表了“半个变 量”的数值。不过这种读取到“半个变量”的情况是非常罕见的,经过实际测试,在目前主流平台下商 用的64位Java虚拟机中并不会出现非原子性访问行为,但是对于32位的Java虚拟机,譬如比较常用的32 位x86平台下的HotSpot虚拟机,对long类型的数据确实存在非原子性访问的风险。从JDK 9起, HotSpot增加了一个实验性的参数-XX:+AlwaysAtomicAccesses(这是JEP 188对Java内存模型更新的 一部分内容)来约束虚拟机对所有数据类型进行原子性的访问。而针对double类型,由于现代中央处 理器中一般都包含专门用于处理浮点数据的浮点运算器(Floating Point Unit,FPU),用来专门处理 单、双精度的浮点数据,所以哪怕是32位虚拟机中通常也不会出现非原子性访问的问题,实际测试也 证实了这一点。笔者的看法是,在实际开发中,除非该数据有明确可知的线程竞争,否则我们在编写 代码时一般不需要因为这个原因刻意把用到的long和double变量专门声明为volatile。

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

       如果要把一个变量从主内存拷贝到工作内存,那就要按顺序执行read和load操作,如果要把变量从 工作内存同步回主内存,就要按顺序执行store和write操作。注意,Java内存模型只要求上述两个操作 必须按顺序执行,但不要求是连续执行。也就是说read与load之间、store与write之间是可插入其他指令 的,如对主内存中的变量a、b进行访问时,一种可能出现的顺序是read a、read b、load b、load a。除此 之外,Java内存模型还规定了在执行上述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操作)。

           这8种内存访问操作以及上述规则限定,再加上稍后会介绍的专门针对volatile的一些特殊规定,就已经能准确地描述出Java程序中哪些内存访问操作在并发下才是安全的。这种定义相当严谨,但也是极为烦琐,实践起来更是无比麻烦。可能部分读者阅读到这里已经对多线程开发产生恐惧感了,后来 Java设计团队大概也意识到了这个问题,将Java内存模型的操作简化为read、write、lock和unlock四种,但这只是语言描述上的等价化简,Java内存模型的基础设计并未改变,即使是这四操作种,对于普通用户来说阅读使用起来仍然并不方便。不过读者对此无须过分担忧,除了进行虚拟机开发的团队外,大概没有其他开发人员会以这种方式来思考并发问题,我们只需要理解Java内存模型的定义即可。

volatile

三重功效:

  1. 64位写入的原子性、
  2. 内存可见性
  3. 禁止重排序(实现有序性)。

happens-before

        介绍这种定义的一个等效判断原则——先行发生原则(Happens-Before),用来确定一个操作在并发环境下 是否安全的。

  • happen-before只确保如果A在B之前执行,则A的执行结果必须对B可见。
    • 单线程:as-if-serial
    • 对volatile变量的写,happen-before于后续对这个变量的读。 --编译器和cpu不能重排序
    • 对synchronized的解锁,happen-before于后续对这个锁的加锁。
    • 对final变量的写,happen-before于final域对象的读,happen-before于后续对final变量的读。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值