欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

JAVA内存模型

程序员文章站 2022-05-31 20:54:13
...

这篇日志主要是来记录我在学习java内存模型的时候,需要了解的知识点。关于java内存模型这部分内容网上也有很详细的资料,这篇主要是做一个知识的梳理,总结。

什么是java内存模型?

在学习java内存模型的时候,我去网上找了很多资料,我发现大部分的文章,讲述的java内存模型都是这样子的

JAVA内存模型

主要是就是在介绍关于堆、栈啊,方法区,程序计数器之类的,这里需要明确一点,以上的模型图,是在描述java虚拟机的内存结构,和我们要分析的内存模型并不相同!

为什么要了解java内存模型?

对于并发编程,我们需要解决的主要问题就是,线程之间数据如何同步问题。在共享内存的并发模型里,线程之间共享程序的公共状态,通过读-写内存中的公共状态进行隐式通讯。

想要把java内存模型描述清楚我觉得是个比较庞大的工程,所以先从java内存模型当中我们常提到的几个概念入手

指令重排序

    在执行程序时,为了提高性能,编译器和处理器常常会对指令做重排序,重排序分为以下三种类型

    1、编译器优化的重排序--编译器在不改变单线程程序语义的情况下,可以重新安排语句的执行顺序

    2、指令级并行的重排序--如果不存在数据依赖,处理器可以改变语句对应机器执行的语句顺序

    3、内存系统的重排序--由于处理器使用缓存和读/写缓存,这使得加载和存储操作看上去像是在乱序执行

在执行程序时,java内存模型确保在不同的编译器和不同的处理器平台上,来插入内存屏障来禁止特定的编译器重排序和处理器重排序,从而为上层提供内存一致性的条件。

happens-before    

JDK1.5开始,java使用JSR-133内存模型,JSR-133使用happens-before的概念来阐述操作之间的内存可见性。在JMM当中,如果一个操作执行的结果需要多另一个操作可见,那么这两个操作之间必须存在happens-before关系。

happens-before的规则如下:

    程序顺序规则:一个线程中的每个操作,happens-before于该线程中的任意后续操作

    监视器锁规则:对每个锁的解锁操作,happens-before于随后对这个锁的加锁。

    volatile变量规则:对一个volatile域的写,happens-before于任意后续对这个volatile域的读

    传递性:A happens-before B, B happens-before C, 则 A happens-before C 

注意:两个操作之间具有happens-before关系,并不意味着前一个操作一定要在后一个操作后面执行,只需要前一个操作的结果对后一个操作的结果可见

as-if-serial语义

    as-if-serial语义:不管怎么重排序(编译器和处理器为了提高并行度),单线程程序的执行结果不能被改变。编译器,runtime和处理器都必须遵守as-if-serial语义。

    如果数据之间存在依赖性,并不会进行重排序,因为这个重排序会影响执行结果。

基于以上一些概念,我们来看下java当中提供了哪些操作,是可以帮助程序员把代码的并发请求发送到编译器

volatile关键字

    volatile主要是为了进行线程之间的通讯,对于共享变量,一个线程对变量的修改,在另外一个线程读取这个变量时,拿到的是最新修改的结果数据。volatile的实现是通过在编译器生成字节码时,在指令序列当中添加内存屏障,来禁止特定类型的指令重排序。

JMM内存屏障的插入策略

    在每个volatile写操作的前面插入一个StoreStore屏障

    在每个volatile写操作的后面插入语一个StoreLoad屏障

    在每个volatile读操作的后面插入一个LoadLoad屏障

    在每个volatile读操作的后面插入一个LoadStore屏障

因此volatile修饰的变量在进行读写操作时,其前面的变量的相关操作一定是早于volatile变量的操作的,其后面的操作一定是晚于volatile变量的操作,volatile变量的读/写 阻止了指令重排序

final域的内存语义

    对于final域,编译器和处理器要遵守两个重排序原则

    1、在构造函数内对一个final变量的写入,与随后把这个构造对象的引用赋值给一个引用变量,这两个操作不能重排序

    2、初次读一个包含final变量的的对象的引用,与随后初次读这个final对象,这两个操作之间不能重排序

关于这两条举个例子

public class A{
    final int i;
    int j;
    A a;
public A(int i, int j){
    this.i = i;
    this.j = j;
}
public void getInstance(){
    a = new A();
}
public void get(){
    A a = a;
    int x = a.i;
    int y = a.j;
}
}

这个类当中,i是final类型的,对于第一条语义的含义,getInstance方法内是对引用类型A的创建,和a引用指向堆内存,对A变量的创建一定是晚于构造方法当中的对i的赋值,也就是说,对final变量的赋值一定是在A这个引用类型变量之前;第二条语义,我们可以看get方法,其实就是A a = a; 和 int x = a.i ;这两条语句之前一定是先读A a = a; 再去执行 int x = a.i ;

关于final变量写的重排序

也就是说JMM禁止编译器把final变量的写重排序到构造函数之外,具体实现就是,编译器会在final变量的写之后,构造函数return之前插入一个StoreStore屏障,这个屏障会禁止处理器把final变量的写重排序到构造函数之外。

读final变量的重排序

在一个线程当中,初次读对象引用A和初次读对象A所包含的final变量i, JMM禁止这两个操作重排序,编译器会在读final变量的操作的前面插入一个LoadLoad屏障。

上面的变量final是基础类型,如果是final变量是引用类型会怎么样

public class B{
   final int[] a;
   static B b;
   public B(){    //构造函数
     a = new int[1]; // 1
     a[0] = 1;  //2
 }
  public static void writer(){ //写线程A
    b = new B(); //3
}
  public static void writer2(){ //写线程B
     b.a[0]=2;  //4
}
   public static void reader(){ //读线程C
     if(b != null)  //5
        int i = b.a[0]; // 6
}
}

对于上面的示例

    步骤1,是对final变量的写入,步骤2 是对final变量引用的对象的成员进行赋值,步骤3 是把对象的引用赋值给某个引用变量,基于前面的例子我们了解到1 和 3 不能重排序的,同时2 和 3 也不能重排序

线程C是可以看到线程A在构造函数当中对final变量对象的成员变量的赋值,所以线程C是可以看到数组下标0的值为1,而线程B对数组元素的写入对线程C并不保证可见。

synchronized的内存语义

    synchronized关键字所修饰的方法或者代码块,都提供了线程安全的条件,原因是某一时刻只能有一个线程持有对象的锁,除了可以实现线程之间互斥访问的功能,synchronized也提供了线程在同步代码块之间写入的操作,对后面访问代码块的线程而言是可见的。在一个线程退出monitor时,会把本地缓存当中的数据刷新到主内存中去,在进入monitor监视器之前会使缓存当中的数据失效,使得变量从主内存中从新加载数据。

参考:《java并发编程的艺术》

            java内存模型

            什么是Java内存模型

相关标签: java内存模型