【知识积累】深入理解Java内存模型(JMM)

一、什么是JMM?

java内存模型(即java memory model,简称JMM),本身是一种抽象的概念,并不真实存在,它描述的是一组规则或规范,通过这组规范定义了程序中各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式。

二、概念区分

JVM内存模型:描述的是多线程允许的行为。
JVM内存结构:描述的是线程运行所设计的内存空间。

建议大家研读《深入理解Java虚拟机(第2版)》

三、抽象示意图

由于JVM运行实例的实体是线程,而每个线程创建时都会为其创建一个工作内存,有些地方称为栈空间,用于存储线程私有的数据。而java内存模型中,规定所有变量都存储在主内存中,主内存是共享内存区域,所有线程都可以访问,但线程对变量的操作(即读取赋值等),必须在工作内存中进行。

首先将变量从主内存拷贝到自己的工作内存当中,然后对变量进行操作,操作完成后再将变量写回主内存,不能直接操作主内存中的变量。工作内存中存储着主内存的变量的副本拷贝,工作内存是每个线程的私有区域,因此不同线程间无法访问对方的工作内存,线程间的通信,也就是传值必须通过主内存来完成

四、JMM的主内存

1、所有线程创建的实例对象都存放在主内存中

2、包括成员变量、类信息、常量、静态变量等

3、由于是共享数据区域,多个线程对同一个变量进行访问时,可能会引发线程安全问题

五、JMM的工作内存

1、主要存储当前方法的所有本地变量信息,其实工作内存存储的是主内存中的变量副本的拷贝,每个线程只能访问自己的工作内存,即线程中的本地变量对其他线程不可见的,就算两个线程执行的是同一段代码,它们也会各自在自己的工作内存中创建属于当前线程的本地变量。

2、字节码行号指示器、Native方法信息

3、由于工作线程是每个线程的私有数据,线程间无法相互访问工作内存,因此存储在工作内存的数据,不存在线程的安全问题

六、主内存与工作内存的数据存储类型以及操作方式归纳

1、根据虚拟机规范,对于一个实例对象中的成员方法而言,如果方法中包含本地变量是基本数据类型的,比如说blooean、byte、short、char、int、long、float、double等,这些本地变量将直接存在在工作内存的栈帧结构中;

2、但倘若本地变量是引用类型的,那该变量的引用会存储在工作内存的栈帧中,而对象实例将存储在主内存,即数据共享区域堆当中;

3、但对于实例对象的成员变量(不管是基本数据类型或者包装类型,还是引用类型)、static变量、类信息均会被存储在主内存中;

4、在主内存中的实例对象,可以被多线程共享,倘若两个线程同时调用了同一个线程的同一个方法,那么两个线程会将要操作的数据拷贝一份到自己的工作内存中,执行完成操作之后,才刷新到主内存里面。

七、JMM如何解决可见性问题

把数据从内存加载到缓存、寄存器,然后运算结束,然后写回主内存。

如果处理器对某个变量进行了修改,可能只是体现在该内核的缓存里,这是个本地状态,而运行在其他内核上的线程,可能加载的是旧状态,这很可能导致一致性的问题,从理论上说,多线程共享引入了复杂的数据依赖性,不管编译器、处理器怎么做、重排序都必须尊重数据依赖性的要求,否则就打破了数据的正确性,这就是JMM所要解决的问题。

八、指令重排序需要满足的条件

在执行程序的时候,为了提高性能,处理器和编译器常常会对指令进行重排序,但是不能随意重排序,即不是想怎么排序就怎么排序,它需要满足一下两个条件:

  • 但单线程环境下不能改变程序运行的结果
  • 存在数据依赖关系的不允许重排序

其实这两点可以归结为一点:
无法通过happens-before原则推导出来的,才能进行指令的重排序。

JMM内部的实现通常是依赖于所谓的内存屏障,通过禁止某些重排序的方式,提供内存可见性保证,也就是实现了各种happens-before的规则。与此同时,更多的复杂度在于需要尽量确保各种编译器、各种体系结构的处理器能够提供一致的行为。

在JMM中,如果一个操作执行的结果需要对另外一个操作可见,那么这两个操作之间必须存在happends-before的关系。

happends-before原则非常重要,它是判断数据是否存在竞争、线程是否安全的主要依据。依靠这个原则,我们便能解决在并发环境下,两个操作之间存在冲突的问题。

九、happens-before的八大原则

1、程序次序规则

一段代码在单线程中执行的结果,是有序的。注意是执行结果是有序的,因为虚拟机处理器会对指令重排序,虽然重排序了,但并不会影响程序的执行结果,所以程序最终执行的结果与顺序执行的结果是一致的,故而这个规则只对单线程有效,在多线程环境下就无法保证准确性了;

2、锁定规则

无论是单线程环境还是多线程环境中,一个锁处于被锁定的状态,那么必须执行unlock操作,后面才能进行lock操作;

3、votile变量规则

这是一条比较重要的规则,它标志着volatile保证了线程的可见性,通俗的讲,如果线程先去写一个volatile的变量,然后线程去读这个变量,那么这个写操作一定是happends-before这个读操作;

4、传递规则

如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C;

5、线程启动规则

Thread对象的start()方法先行发生于此线程的每一个动作;

6、线程中断规则

假设两个线程A和B,A先做了一些操作(即operation A),然后调用B线程的interrupte方法,当B线程感知到自己的中断标识被设置的时候,operation A中的操作结果对B是可见的,也就是operation A调用interrupte方法,那B的标志位就已经变化了,B也能感知的到;

7、线程终结规则

线程A在执行的过程中,通过自定Thread B.join去等待线程B终止,那么线程B在终止之前,对共享变量的修改,在线程A等待返回后是可见的;

8、对象终结规则

结束和开始表明在时间上,一个对象的构造函数必须在它的finalize方法调用时执行完,根据这条规则,可以确保在对象的finalize方法执行时,该对象的所有filed字段值都是可见的。

十、happens-before的概念

如果两个操作不满足上述任意一条happens-before规则,那么这两个操作就没有顺序的保障,JVM可以对这两个操作进行重排序;

如果操作A happens-before操作B,那么操作A在内存上所做的操作对操作B都是可见的。

举例:

    private int value = 0;

    public int read(){
        return value;
    }

    public void write(int value){
        this.value = value;
    }

如果线程A执行write方法,而线程B执行read方法,且线程A优先于线程B执行,那么线程B获得的结果是什么呢?

我们开始分析happens-before八大原则,5、6、7、8不满足,因为无关;1不满足,因为是多线程;2不满足,因为没有使用锁;3不满足,因为value没有使用volatile修饰;4不满足,不存在传递。

所以我们无法通过happens-before原则推导出A happens-before B。所以这段代码是线程不安全的。

如何解决?

满足规则2和规则3其中一个即可,即使用synchronize或者volatile。

    private volatile int value = 0;

    public int read(){
        return value;
    }

    public void write(int value){
        this.value = value;
    }
    private int value = 0;

    public int read(){
        return value;
    }

    public synchronized void write(int value){
        this.value = value;
    }

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章