浅谈Java的内存模型以及交互

本文的内存模型只写虚拟机内存模型,物理机的不予描述。

Java内存模型

  在Java中,虚拟机将运行时区域分成6中,如下图:

                

  1. 程序计数器:用来记录当前线程执行到哪一步操作。在多线程轮换的模式中,当当前线程时间片用完的时候记录当前操作到哪一步,重新获得时间片时根据此记录来恢复之前的操作。
  2. 虚拟机栈:这就是我们平时所说的栈了,一般用来储存局部变量表、操作数表、动态链接等。
  3. 本地方法栈:这是另一个栈,用来提供虚拟机中用到的本地服务,像线程中的start方法,JUC包里经常使用的CAS等方法都是从这来的。
  4. 堆:主要的储存区域,平时所创建的对象都是放在这个区域。其内部还分为新生代、老年代和永久代(也就是方法区,在Java8之后删除了),新生代又分为两块Survivor和一块Eden,平时创建的对象其实都是在Eden区创建的,不过这些之后再跟垃圾回收器写在一篇文章。
  5. 方法区:储存符号引用、被JVM加载的类信息、静态变量的地方。在Java8之后方法区被移除,使用元空间来存放类信息,常量池和其他东西被移到堆中(其实在7的时候常量池和静态变量就已经被移到堆中),不再有永久代一说。删除的原因大致如下:
    1. 容易造成内存溢出或内存泄漏,例如 web开发中JSP页面较多的情况。
    2. 由于类和方法的信息难以确定,不好设定大小,太大则影响年老代,太小容易内存溢出。
    3. GC不好处理,回收效率低下,调优困难。
  6. 常量池:存放final修饰的成员变量、直接定义的字符串(如 Sring s = "test";这种)还有6种数据类型包装类型从-128~127对应的对象(这也解释了我们new两个在这区间的包装类型对象时,为什么他们是一样的,布尔类型存放的是true和false两种,浮点类型Double和Float因为精度问题不存入其中)等

在上面的6种类型中,前三种是线程私有的,也就是说里面存放的值其他线程是看不到的,而后面三种(真正意义上讲只有堆一种)是线程之间共享的,这里面的变量对于各个线程都是可见的。如下图所示,前三种存放在线程内存中,大家都是相互独立的,而主内存可以理解为堆内存(实际上只是堆内存中的对象实例数据部分,其他例如对象头和对象的填充数据并不算入在内),为线程之间共享:

                      

Java内存之间的变量交互

  这里的变量指的是可以放在堆中的变量,其他例如局部变量、方法参数这些并不算入在内。线程内存跟主内存变量之间的交互是非常重要的,Java虚拟机把这些交互规范为以下8种操作,每一种都是原子性的(非volatile修饰的Double和Long除外)操作。

  1. Lock(锁)操作:操作对象为线程,作用对象为主内存的变量,当一个变量被锁住的时候,其他线程只有等当前线程解锁之后才能使用,其他线程不能对该变量进行解锁操作。
  2. Unlock(解锁)操作:同上,线程操作,作用于主内存变量,令一个被锁住的变量解锁,使得其他线程可以对此变量进行操作,不能对未锁住的变量进行解锁操作。
  3. Read(读):线程从主内存读取变量值,load操作根据此读取的变量值为线程内存中的变量副本赋值。
  4. Load(加载):将Read读取到的变量值赋到线程内存的副本中,供线程使用。
  5. Use(使用):读取线程内存的作用值,用来执行我们定义的操作。
  6. Assign(赋值):在线程操作中变量的值进行了改变,使用此操作刷新线程内存的值。
  7. Store(储存):将当前线程内存的变量值同步到主内存中,与write操作一起作用。
  8. Write(写):将线程内存中store的值写入到主内存中,主内存中的变量值进行变更。

  可能有同学会不理解read和load、store和write的区别,觉得这两对的操作类似,可以这样理解:一个是申请操作,另一个是审核通过(允许赋值)。例如:线程内存A向主内存提交了变更变量的申请(store操作),主内存通过之后修改变量的值(write操作)。可以通过下面的图来理解:

参照《深入理解Java虚拟机》

  对于普通的变量来说(非volatile修饰的变量),虚拟机要求read、load有相对顺序即可,例如从主内存读取i、j两个变量,可能的操作是read i->read j->load j-> load i,并不一定是连续的。此外虚拟机还为这8种操作定制了操作的规则:

  • (read,load)、(store,write)不允许出现单独的操作。也就是说这两种操作一定是以组的形式出现的,有read就有load,有store就有write,不能读取了变量值而不加载到线程内存中,也不能储存了变量值而不写到主内存中。
  • 不允许线程放弃最近的assign操作。也就是说当线程使用assign操作对私有内存的变量副本进行了变更的时候,其必须使用write操作将其同步到主内存当中去。
  • 不允许一个线程无原因地(没有进行assign操作)将私有内存的变量同步到主内存中。
  • 变量必须从主内存产生,即不允许在私有内存中使用未初始化(未进行load或者assgin操作)的变量。也就是说,在use之前必须保证执行了load操作,在store之前必须保证执行了assign操作,例如有成员变量a和局部变量b,如果想进行a = b的操作,必须先初始化b。(一开始说了,变量指的是可以放在堆内存的变量)
  • 一个变量一次只能同时允许一个线程对其进行lock操作。一个主内存的变量被一个线程使用lock操作之后,在这个线程执行unlock操作之前,其他线程不能对此变量进行操作。但是一个线程可以对一个变量进行多次锁,只要最后释放锁的次数和加锁的次数一致才能解锁。
  • 当线程使用lock操作时,清除所有私有内存的变量副本。
  • 使用unlock操作时,必须在此操作之前将变量同步到主内存当中。
  • 不允许对没有进行lock操作的变量执行unlock操作,也不允许线程去unlock其他线程lock的变量。

改变规则的Volatile关键字

  对于关键字volatile,大家都知道其一般作为并发的轻量级关键字,并且具有两个重要的语义

  1. 保证内存的可见性:使用volatile修饰的变量在变量值发生改变的时候,会立刻同步到主内存,并使其他线程的变量副本失效。
  2. 禁止指令重排序:用volatile修饰的变量在代码语句的前后会加上一些内存屏障来禁止指令的重新排序。

但这两个语义都是因为在使用volatile关键字修饰变量的时候,内存间变量的交互规则会发生一些变化:

  1. 在对变量执行use操作之前,其前一步操作必须为对该变量的load操作;在对变量执行load操作之前,其后一步操作必须为该变量的use操作。也就是说,使用volatile修饰的变量其read、load、use都是连续出现的,所以每次使用变量的时候都要从主内存读取最新的变量值,替换私有内存的变量副本值(如果不同的话)。
  2. 在对变量执行assign操作之前,其后一步操作必须为store;在对变量执行store之前,其前一步必须为对相同变量的assign操作。也就是说,其对同一变量的assign、store、write操作都是连续出现的,所以每次对变量的改变都会立马同步到主内存中。
  3. 在主内存中有变量a、b,动作A为当前线程对变量a的use或者assign操作,动作B为与动作A对应load或store操作,动作C为与动作B对应的read或write操作;动作D为当前线程对变量b的use或assign操作,动作E为与D对应的load或store操作,动作F为与动作E对应的read或write操作;如果动作A先于动作D,那么动作C要先于动作F。也就是说,如果当前线程对变量a执行的use或assign操作在对变量buse或assign之前执行的话,那么当前线程对变量a的read或write操作肯定要在对变量b的read或write操作之前执行。

从上面volatile的特殊规则中,我们可以知道1、2条其实就是volatile内存可见性的语义,第三条就是禁止指令重排序的语义。另外还有其他的一些特殊规则,例如对于非volatile修饰的double或者long这两个64位的数据类型中,虚拟机允许对其当做两次32位的操作来进行,也就是说可以分解成非原子性的两个操作,但是这种可能性出现的情况也相当的小。因为Java内存模型虽然允许这样子做,但却“强烈建议”虚拟机选择实现这两种类型操作的原子性,所以平时不会出现读到“半个变量”的情况。

volatile不具备原子性

  虽然volatile修饰的变量可以强制刷新内存,但是其并不具备原子性,稍加思考就可以理解,虽然其要求对变量的(read、load、use)、(assign、store、write)必须是连续出现,即以组的形式出现,但是这两组操作还是分开的。比如说,两个线程同时完成了第一组操作(read、load、use),但是还没进行第二组操作(assign、store、write),此时是没错的,然后两个线程开始第二组操作,这样最终其中一个线程的操作会被覆盖掉,导致数据的不准确。如果你觉得这是JOJO的奇妙比喻,可以看下面的代码来理解

public class TestForVolatile {

    public static volatile int i = 0;

    public static void main(String[] args) throws InterruptedException {
        // 创建四个线程,每个线程对i执行一定次数的自增操作
        new Thread(() -> {
            int k = 0;
            while (k++ < 10000) {
                i++;
            }
            System.err.println("线程" + Thread.currentThread().getName() + "执行完毕");
        }).start();
        new Thread(() -> {
            int k = 0;
            while (k++ < 10000) {
                i++;
            }
            System.err.println("线程" + Thread.currentThread().getName() + "执行完毕");
        }).start();
        new Thread(() -> {
            int k = 0;
            while (k++ < 10000) {
                i++;
            }
            System.err.println("线程" + Thread.currentThread().getName() + "执行完毕");
        }).start();
        new Thread(() -> {
            int k = 0;
            while (k++ < 10000) {
                i++;
            }
            System.err.println("线程" + Thread.currentThread().getName() + "执行完毕");
        }).start();
     // 睡眠一定时间确保四个线程全部执行完毕
        Thread.sleep(1000);      // 最终结果为33555,没有预期的4W
        System.out.println(i);      
    }

}

 结果图:

  解释一下:因为i++操作其实为i = i + 1,假设在主内存i = 99的时候同时有两个线程完成了第一组操作(read、load、use),也就是完成了等号后面变量i的读取操作,这时候是没问题的,然后进行运算,都得出i+1=100的结果,接着对变量i进行赋值操作,这就开始第二组操作(assign、store、write),是不是同时赋值的无所谓,这样一来,两个线程都会以i = 100把值写到主内存中,也就是说,其中一个线程的操作结果会被覆盖,相当于无效操作,这就导致上面程序最终结果的不准确。

  如果要保证原子性的话可以使用synchronize关键字,其可以保证原子性内存可见性(但是不具备有禁止指令重排序的语义,这也是为什么double-check的单例模式中,实例要用volatile修饰的原因);当然你也可以使用JUC包的原子类AtomicInteger之类的。

  暂时写到这里,其他关于重排序、内存屏障和happens-before原则等内容后面再进行补充。如果文章有任何不对的地方望大家指出,感激不尽!

原文地址:https://www.cnblogs.com/zhangweicheng/p/11638841.html

时间: 10-09

浅谈Java的内存模型以及交互的相关文章

浅谈 Java Printing

浅谈 Java  Printing 其实怎么说呢?在写这篇博文之前,我对java printing 可以说是一无所知的.以至于我在敲文字时, 基本上是看着api文档翻译过来的.这虽然看起来非常的吃力,但是我相信,有道大哥不会辜负我的.嘻 嘻! Java Printing 技术,也就是我们平时所接触的打印,只不过是说可以用Java实现而已. 一.Java Printing 打印简介 Java Printing API能够使java应用程序实现相关的打印功能,如: 1.打印所有 Java 2D 和

【转】浅谈Java中的equals和==

浅谈Java中的equals和== 在初学Java时,可能会经常碰到下面的代码: 1 String str1 = new String("hello"); 2 String str2 = new String("hello"); 3 4 System.out.println(str1==str2); 5 System.out.println(str1.equals(str2)); 为什么第4行和第5行的输出结果不一样?==和equals方法之间的区别是什么?如果在初

浅谈java异常[Exception]

本文转自:focusJ 一. 异常的定义 在<java编程思想>中这样定义 异常:阻止当前方法或作用域继续执行的问题.虽然java中有异常处理机制,但是要明确一点,决不应该用"正常"的态度来看待异常.绝对一点说异常就是某种意义上的错误,就是问题,它可能会导致程序失败.之所以java要提出异常处理机制,就是要告诉开发人员,你的程序出现了不正常的情况,请注意. 记得当初学习java的时候,异常总是搞不太清楚,不知道这个异常是什么意思,为什么会有这个机制?但是随着知识的积累逐渐也

浅谈Java中的equals和==

浅谈Java中的equals和== 在初学Java时,可能会经常碰到下面的代码: 1 String str1 = new String("hello"); 2 String str2 = new String("hello"); 3 4 System.out.println(str1==str2); 5 System.out.println(str1.equals(str2)); 为什么第4行和第5行的输出结果不一样?==和equals方法之间的区别是什么?如果在初

浅谈Java虚拟机

最近发现MDT推出去的系统的有不同问题,其问题就不说了,主要是策略权限被域继承了.比如我们手动安装的很多东东都是未配置壮态,推的就默认为安全壮态了,今天细找了一下,原来把这个关了就可以了. 浅谈Java虚拟机,布布扣,bubuko.com

浅谈Java中的对象和引用

浅谈Java中的对象和对象引用 在Java中,有一组名词经常一起出现,它们就是"对象和对象引用",很多朋友在初学Java的时候可能经常会混淆这2个概念,觉得它们是一回事,事实上则不然.今天我们就来一起了解一下对象和对象引用之间的区别和联系. 1.何谓对象? 在Java中有一句比较流行的话,叫做"万物皆对象",这是Java语言设计之初的理念之一.要理解什么是对象,需要跟类一起结合起来理解.下面这段话引自<Java编程思想>中的一段原话: "按照通

浅谈 Unix I/O 模型

原文出处:http://miaoo.in/talk-about-unix-io-model.html 在实际应用中,数据操作通常分为输入和输出,那么以输入为例,在操作系统中,一个数据的输入通常分为以下两个过程: a. 等待数据准备好.b. 将准备好的数据从内核拷贝到用户空间. 下面我们将会分别讨论 I/O 模型中的两个大类,即 同步 I/O 与 异步 I/O. 1. 同步 I/O 同步与异步 I/O 的最大不同,就是在在进行数据复制时(即过程 b ),所有的同步 I/O 模型均会发生阻塞.进一步

Java虚拟机内存模型及垃圾回收监控调优

Java虚拟机内存模型及垃圾回收监控调优 如果你想理解Java垃圾回收如果工作,那么理解JVM的内存模型就显的非常重要.今天我们就来看看JVM内存的各不同部分及如果监控和实现垃圾回收调优. JVM内存模型         正如你上图所看到的,JVM内存可以划分为不同的部分,广义上,JVM堆内存可以划分为两部分:年轻代和老年代(Young Generation and Old Generation) 年轻代(Young Generation) 年轻代用于存放由new所生成的对象.当年轻代空间满时,

java的内存模型

java内存模型 Java虚拟机规范中试图定义一种Java内存模型(Java Memory Model,JMM)来屏蔽掉各种硬件和操作系统的访问差异,以实现让Java程序在各种平台下都能达到一致的内存访问效果.在此之前,主流程序语言(如C/C++等)直接使用物理硬件和操作系统的内存模型,因此,会由于不同平台上内存模型的差异,有可能导致程序在一套平台上并发完全正常,而在另外一套平台上并发访问却经常出错,因此在某些场景下就不许针对不同的平台来编写程序.Java内存模型即要定义得足够严谨,才能让Jav

关Java的内存模型(JMM)

JMM的关键技术点都是围绕着多线程的原子性.可见性和有序性来建立的 一.原子性(Atomicity) 原子性是指一个操作是不可中断的.即使是在多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程干扰. 比如,对于一个静态全局变量int i,两个线程同时对它赋值,线程A给他赋值1,线程B给它赋值为-1.那么不管这2个线程以何种方式.何种步调工作,i的值要么是1,要么是-1.线程A和线程B之间是没有干扰的.这就是原子性的一个特点,不可被中断. 但如果我们不使用int型而使用long型的话,可能