zoukankan      html  css  js  c++  java
  • Java 内存模型- Java Memory Model

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

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

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

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

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

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

      

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

    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。看图:

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

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

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

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

      因为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关键字或者是锁来进行处理。也就是在同一时间只能有一个线程去处理这个字段或者方法,同时程序也是从主内存读取数据,然后计算完成后将程序写入到主内存中保证保证计算的有序处理。

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

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

  • 相关阅读:
    函数
    python操作文件
    POJ-2689-Prime Distance(素数区间筛法)
    POJ-2891-Strange Way to Express Integers(线性同余方程组)
    POJ-2142-The Balance
    POJ-1061-青蛙的约会(扩展欧几里得)
    Educational Codeforces Round 75 (Rated for Div. 2) D. Salary Changing
    Educational Codeforces Round 75 (Rated for Div. 2) C. Minimize The Integer
    Educational Codeforces Round 75 (Rated for Div. 2) B. Binary Palindromes
    Educational Codeforces Round 75 (Rated for Div. 2) A. Broken Keyboard
  • 原文地址:https://www.cnblogs.com/huangqingshi/p/7689752.html
Copyright © 2011-2022 走看看