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内存模型

  • 1
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值