迟到的JAVA内存模型

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/yueloveme/article/details/88384937

给大家推荐个靠谱的公众号程序员探索之路,大家一起加油

JAVA内存模型应该在介绍aqs的时候就发出来的,今天回顾的时候才发现没有

强烈建议JAVA-coder看一下深入理解java虚拟机,虽然不会提高编码效率,但是它真的会解决知其然而不知其所以然。

为什么定义JAVA内存模型

为了屏蔽掉各种硬件和操作系统的内存访问差异,以实现让JAVA程序在各种平台下都能达到一致的内存访问效果。

JAVA内存模型中定义的主内存与工作内存

JAVA内存模型规定了所有的变量都存储在主内存(Main Memory)中,物理硬件的名字和作用类似,但是JAVA内存模型中的主内存只是JVM内存中的一部分。与主内存对应的,每个线程都有自己的工作内存,可以与物理硬件的缓存类比,注意这里是每个线程都有自己的工作内存(Working Memory)。工作内存的内容是该线程使用的变量的主内存的副本,注意这个内存副本不是完完全全和主内存一样,例如主内存中一个对象又10MB大小,但是副本只用到了这个对象的其中一个属性,那么副本就指拷贝着一个属性而已。

不同的线程至今啊也无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成(这点是不是和物理硬件的数据传递类似)。

线程,主内存,工作内存示意图:

主内存和工作内存与堆/栈/方法区并不是同一层次的划分,不过从主内存/工作内存的定义来看,主内存主要对应于JAVA堆中的对象的实例数据部分(JAVA堆除了实例数据还保存了对象的GC标记,GC年龄,同步锁等信息),工作内存主要对应虚拟机栈的部分区域。

内存交互操作

8种操作

下面这些操作JAVA内存模型规定必须时原子的(原子的是不是很熟悉),但是对于double,long数据类型有例外

Lock(锁定):作用于主内存的变量,把一个变量标识为一条线程独占的状态

(我理解:这就是为什么使用synchronized关键字后可以达到‘锁’的效果)

Unlock(解锁):作用于主内存的变量,他把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。

Read(读取):作用于主内存的变量,他把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用

Load(载入):作用于工作内存的变量,把read操作从内存中得到的变量值放入工作内存的变量副本中

Use(使用):作用于工作内存,他把工作内存中一个变量的值转递给执行引擎,每当JVM遇到一个需要使用到变量的值的字节码指令时将会执行这个操作

Assign(赋值):作用于工作内存,把一个执行引擎接收到的值赋给工作内存的变量,每当JVM遇到一个需要使用变量赋值的字节码指令时执行这个操作

Store(存储):作用于工作内存变量,它把工作内存中一个变量的值传送到主内存中,以便随后的write操作使用

Write(写入):作用于主内存变量,把store操作从工作内存中得到的变量的值放入主内存的变量中。

如果要把一个变量从主内存复制到工作内存,必须顺序执行read,load操作,如果要把一个变量从工作内存同步到主内存就必须顺序执行store,write操作。注意:JAVA内存模型要求必须顺序执行,但是没有说时连续的。(哈哈到这里是不是就清楚了,不加锁的情况下对于共享资源,为什么存在线程安全问题了吧)

8种限制

  1. 不允许read和load,store和write操作之一单独出现;说的白话一点就是,不允许从主内存读取了变量要到工作内存去,但是工作内存不接受,或者工作内存同步变量到主内存,主内存不接受的情况。
  2. 不允许一个线程丢弃它的最近一次assign操作;就是说工作内存中一个变量改变了就必须同步到主内存中
  3. 不允许一个线程把没有发生过assign操作的变量同步到主内存中
  4. 一个新的变量只能在主内存中产生,不允许工作内存直接使用一个未被初始化(load或assign)的变量。
  5. 一个变量在同一个时刻只能被一条线程lock,但是lock可以被同一条线程重复执行;多次lock后,也需要多次unlock变量才被认为解锁了。(哈哈为什么可以重入)
  6. 如果一个变量执行lock操作,那将会清空工作内存中此变量的值,在执行引擎使用这个变量前,需要虫偶更新执行load或assign操作初始化变量的值。
  7. 如果一个变量之前没有被lock操作过,那就不允许unlock操作,也不允许去unlock一个被其他线程锁定的变量
  8. 对一个变量执行unlock操作之前,必须把变量同步到主内存中

哈哈看完上述之后就明白了‘锁’为什么就能保证线程安全。

Volatile的特殊性

可见性

Volatile变量对所有线程的可见性是指:当一条线程修改了这个变量的值,新值对于其他线程来说时可以立即得知的。

那么问题就来了既然volatile变量的新值对于其他线程是可以立即得知的,那岂不是可以不用加锁,我们来深入看一下就好像,aqs公平锁的时候,那一步是公平的(在第一次请求锁的时候,对于非公平锁第一次请求失败,进入队列,也是要‘排队获取锁的‘)

public static volatile int a = 1;

public static void increase(){

        a++;

}

Javap反编译过来

public static void increase();

Code:

 ………

Getstatic

Iconst_1

Iadd

………..

Getstatic时保证获取最新的a值,下面进行iadd操作呢,不能保证a是最新的值;

(来自深入理解JAVA虚拟机这本书)

只有两种场景不用加锁,volatile就能保证原子性

  1. 运算结果并不依赖变量的当前值,或者能够确保只有单一的线程修改变量的值
  2. 变量不需要与其他的状态变量共同参与不变约束

(很懵逼,刚开始的时候很生涩,需要细细品)

用 final 修饰的类不可以被继承,用 final 修饰的方法不可以被覆写,用 final 修饰的属性一旦初始化以后不可以被修改。现在final对于内存可见性的影响。在对象的构造方法中设置 final 属性,同时在对象初始化完成前,不要将此对象的引用写入到其他线程可以访问到的地方(不要让引用在构造函数中逸出)。如果这个条件满足,当其他线程看到这个对象的时候,那个线程始终可以看到正确初始化后的对象的 final 属性。

上面说得很明白了,final 属性的写操作不会和此引用的赋值操作发生重排序,如:

x.finalField = v; ...; sharedRef = x;

禁止指令重排序

这一块目前是我的薄弱区域,java运行时优化。对于指令重排序而言,现象是,jvm不一定按照代码的顺序去执行代码。但是对于volatile变量来说会加一个内存屏障,指令重排序时不能把后面的指令重排序到内存屏障之前的位置。

介绍一个小插曲,记不记得单例模式的双检测实现方式,其实在1.5以前双检测实现方式还是有问题的,因为volatile屏蔽指令重排序在1.5中才完全修复。

 
public class FatherClass {

   private volatile static FatherClass fatherClass;

   private FatherClass(){

   }

   

   public static FatherClass getFatherClass(){

      if (fatherClass == null){

         synchronized (FatherClass.class){

            if (fatherClass == null){

               fatherClass = new FatherClass();

            }

         }

      }

      return fatherClass;

   }

}

对于volatile变量需要满足的规则

  1. 每次使用volatile变量时都必须从主内存刷新最新值,用于保证能看见其他线程对于volatile变量所修改后的值
  2. 每次修改完volatile变量后都必须立即同步到主内存中,用于保证其他线程能看到修改后的值
  3. Volatile修饰的变量不允许指令重排序,保证代码执行顺序与程序的顺序相同

对于long和double的特殊规则

JVM允许将没有被volatile修饰的64位数据的读写操作划分为两次32位的操作来进行,也就是说可以允许对long,double的read,load,store,write可以是非原子的。有一种很‘奇葩‘的现象,某些线程可能读到一个既不是非原值,也不是修改后的值,代表了’半个变量‘的数值

天然有序代码

这个东东在深入理解java虚拟机12.3.6节,刚开始看很不理解。其实举个例子(以对象创建,回收为例),当一个对象实例没有初始化完成之前,不会调用finalize方法(对象被回收时调用的方法);白话解释对象都没有初始化,你怎么回收。。。。。。。

列举类似天然有序代码,正统的名字叫做先行发生关系(happens-before)

  1. 程序次序规则(Program Order Rule):在一个线程内,按照程序代码顺序,书写在前面的操作先行发生于书写在后面的操作。准确的说,应该是控制流顺序而不是程序代码顺序,因为要考虑分支,循环结构
  2. 管程锁定规则(Monitor Lock Rule):一个unlock操作先行发生于后面对同一个锁的lock操作。这里必须强调的时同一个锁,‘后面‘指的是时间先后顺序
  3. Volatile变量规则(Volatile Variable Rule):对于一个volatile变量的写操作先行发生于后面对于这个变量的读操作,‘后面‘指的是时间先后顺序
  4. 线程启动规则(Thread Start Rule):Thread对象的start()方法先行发生于线程的每一个动作
  5. 线程终止规则(Thread Termination Rule):线程中的所有操作都先行发生于此线程的终止检测,可以通过Thread.join()方法结束,Thrad.isAlive()的返回值等手段检测到线程已经终止执行
  6. 线程中断规则(Thread Interruption Rule):对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过Thread.interrupted方法检测到是否有中断发生
  7. 对象终结规则(Finalizer Rule):一个对象的初始化完成(构造函数执行结束),先行发生于他的finalize方法的开始
  8. 传递性(Transitivity):如果操作A先行发生于操作B,操作B先行发生于操作C,就可以得出操作A先行发生于操作C的结论。

猜你喜欢

转载自blog.csdn.net/yueloveme/article/details/88384937