2、java内存间交互操作

关于主内存与工作内存之间具体的交互协议,即一个变量如何从主内存拷贝到工作内存,如何从工作内存同步回主内存之类的实现细节,java内存模型中定义了8种操作来完成,虚拟机实现时必须保证这8种操作都是原子的、不可分割的(对于long和double类型的变量来说,load、store、read跟write在某些平台上允许例外)。
8种基本操作:
  1. lock,锁定,所用于主内存变量,它把一个变量标识为一条线程独占的状态。
  2. unlock,解锁,解锁后的变量才能被其他线程锁定。
  3. read,读取,所用于主内存变量,它把一个主内存变量的值,读取到工作内存中。
  4. load,载入,所用于工作内存变量,它把read读取的值,放到工作内存的变量副本中。
  5. use,使用,作用于工作内存变量,它把工作内存变量的值传递给执行引擎,当JVM遇到一个变量读取指令就会执行这个操作。
  6. assign,赋值,作用于工作内存变量,它把一个从执行引擎接收到的值赋值给工作内存变量。
  7. store,存储,作用域工作内存变量,它把工作内存变量值传送到主内存中。
  8. write,写入,作用于主内存变量,它把store从工作内存中得到的变量值写入到主内存变量中
8种操作的规则:
  java内存模型还规定了在执行上述8种基本操作时必须满足如下规则:
  1. 不允许read和load、store和write操作之一单独出现,即不允许加载或同步工作到一半。
  2. 不允许一个线程丢弃它最近的assign操作,即变量在工作内存中改变了之后,必须吧改变化同步回主内存。
  3. 不允许一个线程无原因地(无assign操作)把数据从工作内存同步到主内存中。
  4. 一个新的变量只能在主内存中诞生。
  5. 一个变量在同一时刻只允许一条线程对其进行lock操作,但lock操作可以被同一条线程重复执行多次,,多次lock之后必须要执行相同次数的unlock操作,变量才会解锁。
  6. 如果对一个对象进行lock操作,那会清空工作内存变量中的值,在执行引擎使用这个变量前,需要重新执行load或assign操作初始化变量的值。
  7. 如果一个变量事先没有被lock,就不允许对它进行unlock操作,也不允许去unlock一个被其他线程锁住的变量。
  8. 对一个变量执行unlock操作之前,必须将此变量同步回主内存中(执行store、write)。
  有如上8种内存访问操作以及规则限定,再加上对volatile的一些特殊规定,就已经完全确定了java程序中哪些内存访问操作是在并发下安全的。
对于volatile的特殊规则:
  volatile有两个特性:1、对所有线程可见;2、防止指令重排;我们接下来说明一下这两个特性。
  可见性,是指当一条线程修改了某个volatile变量的值,新值对于其它线程来说是可以立即知道的。而普通变量无法做到这点。但这里有个误区,由于volatile对所有线程立即可见,对volatile的写操作会立即反应到其它线程,因此基于volatile的变量的运算在并发下是安全的。这是错误的,原因是volatile所谓的其它线程立即知道,是其它线程在使用的时候会读读内存然后load到自己工作内存,如果这时候其它线程进行了修改,本线程的volatile变量状态会被置为无效,会重新读取,但如果本线程的变量已经被读入执行栈帧,那么是不会重新读取的;那么两个线程都把本地工作内存内容写入主存的时候就会发生覆盖问题,导致并发错误。
  防止指令重排,重排序优化是机器级的操作,也就是硬件级别的操作。重排序会打乱代码顺序执行,但会保证在执行过程中所有依赖赋值结果的地方都能获取到正确的结果,因此在一个线程的方法执行过程中无法感知到重排的操作影响,这也是“线程内表现为串行”的由来。volatile的屏蔽重排序在jdk1.5后才被修复。原理是volatile生成的汇编代码多了一条带lock前缀的空操作的命令,而根据IA32手册规定,这个lock前缀会使得本cpu的缓存写入内存,而写入动作也会引起别的cpu或者别的内核无效化,这相当于对cpu缓存中的变量做了一次store跟write的操作,所以通过这样一个操作,可以让变量对其它cpu立即可见(因为状态被置为无效,用的话必须重新读取)。
  另外,java内存模型对volatile变量有三条特殊规则:
  a、每次使用变量之前都必须先从主内存刷新最新的值,用于保证能看见其它线程对变量的修改;
  b、每次对变量修改后都必须立刻同步到主内存中,用于保证其它线程可以看到自己的修改;
  c、两个变量都是volatile的,将数据同步到内存的时候,先读的先写;
long跟double变量的特殊规则
  对于64位的数据类型long跟double,java内存模型定义了一条相对宽泛的规定:允许虚拟机将没有被volatile修饰的64位数据操作分为两次32位的操作来进行。也就是允许虚拟机不保证64位数据load、store、read跟write这4个操作的原子性,这就是long跟double的非原子性协定。如果真的这样,当多个线程共享一个并未声明为volatile的long或者double类型的变量,并同时对他们进行读取修改,那么某些线程可能会读到一些既非初始值也不是其他线程修改值的代表了“半个变量”的数据。
  不过这种读到“半个变量”的情况非常罕见,因为java内存模型虽然允许实现为非原子的但“强烈建议”将其实现为原子操作,实际开发中,所有商用虚拟机都将其实现为原子操作,因此,这点我们并不需要担心。

posted @ 2018-08-11 22:41  facelessvoidwang  阅读(1095)  评论(0编辑  收藏  举报