Java 内存模型- Java Memory Model

 2019-12-10 15:49  阅读(858)
文章分类:Java Core

  多线程越来越多的使用,使得我们需要对它的深入理解。那么就涉及到了Java内存模型JMM。JMM是JVM的一部分,JMM定义了一个线程修改了一个共享变量,其他线程什么时候或者如何看到这个变量,如何去访问共享变量。

  咱们来看一张图(图片手绘的,字写的不好,见谅),JVM里边分为堆和栈,每一个线程都有一个线程栈,用于区分其他线程。

20191210001321\_1.png

  每个线程的入口是一个run方法,然后run方法开始调用其他方法。在方法中有两种数据类型,一种是原始类型,一种是引用类型。原始类型如( boolean, byte, short, char, int, long, float, double),这种其他线程根本看不到,只在线程中使用(存放在线程调用栈中)。另外一种是引用类型,引用类型比如原始类型的对象类型,比如(Boolean,Byte等),线程使用的时候会在堆中生成一个对象,并且将变量对其进行指向。每个线程使用此种变量的时候会自己创建一个变量(副本)并且指向,只有当前线程知道,其他线程看不到。那么引用对象的成员变量又分为基本类型和原始类型,并且一次进行创建副本并且指向。

   在方法中用到static对象的实例的需要进行区别对待。因为在堆里边只有一个对象,所有线程对其进行引用。此时不是副本,需要注意。那么如果多线程对其惊醒操作的时候会出现值写的不对的,比如两个线程同时对对象里边的成员变量原始int类型count进行加1,如果count初始化的0的话,可能会出现结果为1的情况。

  那么如果传过来的参数分为基本类型和引用类型呢?如果为基本类型,那么就是副本,如果是引用类型的话,就是原始对象的副本和副本指向,在这时需要注意,如果改了内存中对象的属相,那么随之这个对象会发生改变,但是对象的指向不会改变。

  下面咱们通过代码才详细看一下,看之前首先看看图片。

  20191210001321\_2.png

  咱们先进行简单的解释,一个对象,里边有两个方法,第一个方法只有一个原始类型变量,第二个方法有两个变量,一个是原始对象引用类型,另一个是静态对象实例的引用类型。这个图就是他们的内存结构图。下面咱们来看看代码:

package com.hqs.jmm;

    /**
     * 
     * JMM对象
     * @author qs.huang
     *
     */
    public class MyJMMObject implements Runnable{

        @Override
        public void run() {
            methodOne();
        }

        public void methodOne() {
            int var1 = 0; //方法内部变量,原始类型
            methodTwo();
        }

        public void methodTwo(){
            Integer var1 = new Integer(0); //方法内部变量,引用变量
            MyReferenceObj var2 = MyReferenceObj.instance; //静态引用变量
        }

    }

package com.hqs.jmm;

    /**
     * 
     * 引用对象
     * @author qs.huang
     *
     */

    public class MyReferenceObj {
        public static MyReferenceObj instance = new MyReferenceObj();
        public Integer intObj = new Integer(1);
        public int intPrimary = 0;
        //隐藏构造
        private MyReferenceObj(){}
    }

  因为中的var1是方法1的局部变量,也是原始类型,每个用到它的地方,把它放到方法栈中。每一个线程都有自己的栈,所以每个一份。

  方法2中的var1是一个对象类型的也是局部的,每个线程需要在堆里边创建一个对象,同时对它进行指向。

  方法2中var2是一个静态对象实例的引用,所以再堆中只有一份,并且在加载的时候进行的实例化,因为对象中还有引用类型,所以产生了一个对intObj的引用,同时还有一个int原始类型存在堆里,跟随实例对象。

  **那么方法中如果有基本类型的数组呢?**那数组会在堆中生成,然后对象只想堆中的数组对象,多线程的话,每个线程会生成自己所需要的副本,当方法调用完成后,该数组对象就会被收回。

  下面咱们来看看CPU的硬件结构以便大家更理解JMM。看图:

20191210001321\_3.png

  目前的电脑一般都是2个或2个以上的CPU,每个CPU可能是多核的。那么每个CPU在同一时间就可以处理一个线程,多个CPU就可以同一时间执行多个线程。

  每个CPU都有一个寄存器,CPU通过寄存器进行运算,那么在寄存器运算速度要高于在主内存进行运算。

  每个CPU都附带一个缓存,用于将数据从主内存中读取到缓存数据中,然后再运算的时候放到寄存器里。CPU访问寄存器的速度是最快的,访问缓存的速度其次,最后是访问主内存的速度,当然缓存分为L1,L2 两个缓存,当然我画的没有那么好,不过不影响理解。CPU不会读取缓存中的所有数据,而是按照缓存line去进行有选择的读取。

  当CPU执行完相关的运算并在适当的时候将结果刷到主内存RAM中,用于保存结果或让其他程序读取。咱们看一下JVM和CPU之间的关系。

20191210001321\_4.png

  因为CPU没有堆和栈,JVM的堆和栈会在CPU的主内存中,但是程序执行的时候,会将栈或堆中的线程读取到缓存和寄存器中进行运算,并且将计算的结果重新刷新到主内存RAM中。在这个时候因为有多CPU的原因,那么假如说一个CPU一个变量,那么两个并行的线程在执行的时候会有什么样的问题呢?

  比如一个类中只有一个String state的成员变量,一个线程对其进行读取到CPU缓存中,然后将其设置为了’YES’,并放回到缓存中;另一个线程没有看到这个值的更改,因为没有看到起更改。然后将其读取到CPU缓存中,然后设置为’YES’或’NO’。这个就是可见性问题,那么如何实现其他线程可见呢?Java有个关键字volatile,这个关键字可以使得操作不写入CPU缓存,直接从主内存读取,更改后直接重写到主缓存中。

  比如这个类有个int count的成员变量,并且初始化值为0,向前面提到的,一个线程读取到这个count到CPU缓存中,另一个线程也把这个count读取到另一个CPU的线程中,那么两个线程放到寄存器计算,分别对其进行加1操作,这个时候都把结果刷新到缓存并且到主内存中。count的结果变为了1,这个不是大家想要的。因为每个线程对这个变量读取不可见,每个都用其副本进行操作。这个就是线程的竞态条件。那么怎么才能都保证这个变量的正确呢,就是使用同步,也就是使用synchronize关键字或者是锁来进行处理。也就是在同一时间只能有一个线程去处理这个字段或者方法,同时程序也是从主内存读取数据,然后计算完成后将程序写入到主内存中保证保证计算的有序处理。

  这下同学们是否有了新的认识了呢?

  如果有写的不对的地方希望告知~

点赞(0)
版权归原创作者所有,任何形式转载请联系作者; Java 技术驿站 >> Java 内存模型- Java Memory Model

相关推荐