Java内存模型--JMM

Java内存模型 (JMM)和JVM运行时内存的区别

JVM运行时内存

Java运行时内存模型,描述了Java程序代码在运行时,一次执行单个语句或者表达式时(即通过单个线程执行时)不同类型的变量、引用、对象、类等等的一些信息的存储规范

Java内存模型

描述了多个线程运行时的语义规范,比如多个线程修改了共享内存的值时,应该读取到哪个值得规则。

下边我们来看一个多线程读取的一个问题代码

/**
 * 提供一个产生多线程可见性的原因代码
 * 理论上在子线程在主线程将flag改为false之后应该打印出i的值,但实际运行中没有
 */
public class MultiThreadProblem {

    public static boolean flag = true;
    //public volatile static boolean flag = true; //这样就可以保证flag变量的可见性

    public static void main(String[] args) throws InterruptedException {
        //开启一个子线程来根据flag的值进行i++操作
        new Thread(()->{
            int i = 0;
            System.out.println(Thread.currentThread().getName() + "正在运行," + "flag=" + flag);

            while (flag){
                i++;
            }
            System.out.println("flag=false,i=" + i);
        }).start();

        //主线程睡眠3s后改变flag的值
        Thread.sleep(3000L);
        flag =false;
        System.out.println("主线程更改了flag的值为:" + flag);

    }
}

上边问题引发的原因是,两个线程都从内存共享区的方法区复制了一份flag变量的值true放到各自的虚拟机栈中,虽然主线程将更改了flag=false,并且通过也将更改的值覆盖了方法区的值,但是子线程在执行while语句的时候,CPU因为while的时间操作较长而将while语句进行了指令重排。CPU指令重排在单线程中是没有问题的,但是在多线程中操作一个共享变量,指令重排就可能会对结果产生影响。
因为Java语言是介于脚本语言和编译语言之间,当解释器读到while语句的时候,JIT编译器遇到while循环和反复调用的情况,就会将当前的代码进行指令重排。然后这个指令重排,对于多线程来说,如果其他线程对于共享变量有更改的话,就可能产生可见性问题,就像上边代码中将flag改为false后,子线程不会感知的到。

使用了volatile关键字之后,就会在编译的时候告诉JVM有关该变量的操作不进行指令重排,一旦有线程更改了变量之后,就会将该变量写入内存中。

这些关于多线程中共享内存(或者堆内存)中的共享变量冲突问题,就是JMM约束的范围,它描述线程间操作的语义规范。

JMM规定有以下几个:

  1. 对于同步的规则定义参考图片
    同步的规则定义

  2. happens-before先行发生原则,参考图片
    happens-before先行发生原则

  3. 被final定义的对象,对于所有线程都会看到正确的构造版本。通常被static final修饰的字段不能被修改。然而System.in/System.out/System.err被static final修饰,却可以被修改,这是个遗留问题。必须通过set方法改变,我们将这些字段称为写保护,以区别于普通final字段。

  4. Word Tearing(字分裂)字节处理,参考图片
    Word Tearing(字分裂)字节处理

  5. double 和long的特殊处理
    这两个都是64位的,它的单次写操作都是分两次来进行的,每次操作其中32位的时候,可能导致第一次写入后,读取的值是脏数据,第二次写完成后,才能读到正确数据。所以建议用volatile来修饰double和long才行。

posted @ 2020-08-28 17:59  爪哇洋  阅读(88)  评论(0编辑  收藏  举报