`
春花秋月何时了
  • 浏览: 39985 次
  • 性别: Icon_minigender_1
  • 来自: 成都
社区版块
存档分类
最新评论

Java内存模型JMM之五final内存语义

 
阅读更多

 

除了volatile和synchronized可实现可见性之外,final关键字也可以实现可见性(但final域所属的引用不能从构造方法中“逸出”)。synchronized同步块的可见性是由happens-before的锁定规则获得的。下面就来详细的研究一下final关键字的内存语义。

 

对于 final 变量,编译器和处理器都要遵守两个重排序规则:

 

写final域规则在构造函数内对一个final域的写入,与随后把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序。

        该规则禁止把final域的写操作重排序到构造函数之外,因为执行构造函数进行实例化对象,底层可以分为3个操作: 分配内存,在内存上初始化成员变量,把对象实例引用指向内存。这3个操作可能重排序,即先把引用指向内存,再初始化成员变量。

        该规则还是通过内存屏障实现的:编译器会在final域的写之后,构造函数return之前,插入一个StoreStore屏障。从而达到禁止处理器把final域的写重排序到构造函数之外。

        该规则可以保证,在对象引用对任意线程可见之前(但引用不能从构造方法中“逸出”),对象的 final 变量已经正确初始化了,而普通变量则不具有这个保障。

  

读final域规则:初次读一个包含final域的对象的引用,与随后初次读这个final域,这两个操作之间不能重排序。

        该规则禁止把对final域的读操作重排序到读取这个final域所属的对象引用之前,而这是普通变量是无法保证的。

        该规则还是通过内存屏障实现的:编译器会在final域的读操作的前面插入一个LoadLoad屏障。从而达到要读取final域必须先读取final域所属的引用。

        该规则可以保证,在读一个对象的 final 变量之前,一定会先读这个对象的引用。如果读取到的引用不为空表示其对象引用已经对当前读线程可见,根据上面的写final域规则,说明对象的 final 变量一定以及初始化完毕,从而可以读到正确的变量值。

 

public class FinalExample {
    int i;                            //普通变量
    final int j;                      //final变量
    static FinalExample obj;

    public void FinalExample () {     //构造函数
        i = 1;                        //写普通域
        j = 2;                        //写final域
    }

    public static void writer () {    //写线程A执行
        obj = new FinalExample ();
    }

    public static void reader () {       //读线程B执行
        FinalExample object = obj;       //读对象引用
        int a = object.i;                //读普通域
        int b = object.j;                //读final域
    }
}

 

 针对上面的示例代码,这里假设一个线程A先执行writer()方法,模拟执行写操作,随后有线程B执行reader()方法。

  根据读final域规则:线程B对普通域的读操作完全有可能会被重排序到读取对象引用操作之前,从而形成一个错误的读取操作,而对final域的操作则由于读final域规则的保障,一定会先读包含这个final域的对象的引用,在该示例中,如果该引用不为空,那么其final域一定已经被A线程初始化完毕,所以变量b一定为2.

 

  根据写final域规则:线程A对普通域的写入操作完全有可能会被重排序到构造函数之外,但是对final域的写操作则不会。所以(假设线程B读取对象引用操作与读取对象的普通域没有发生重排序):线程B执行读取操作时,一定能够读取到final域的正确初始化后的值2,但是不一定能够读取到普通域初始化之后的值1,而是可能会读取到初始化之前的值0。

 

 引用类型的final域

 上面的示例都是基本类型的final域,如果是引用类型的final域,那么除了必须遵守以上的读写final域规则之外,写 final 域的重排序规则对编译器和处理器增加了如下约束:

 附加的写final域规则:构造函数内,对一个 final 引用的对象的成员域的写入,与随后在构造函数外把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序。

          该规则保证了 对象的final 成员变量在对其他线程可见之前,能够正确的初始化完成。

 

public class FinalReferenceExample {
    final int[] intArray;
    static FinalReferenceExample obj;
 
    public FinalReferenceExample() {
        intArray = new int[1];// 1
        intArray[0] = 1;// 2
    }
 
    public static void writerOne() {// A线程执行
        obj = new FinalReferenceExample(); // 3
    }

    public static void writerTwo () {          //写线程B执行
        obj.intArray[0] = 2;                 //4
    }
 
    public static void reader() {// 读线程 C 执行
        if (obj != null) { // 5
            int temp1 = obj.intArray[0]; // 6
        }
    }
}
 

 

 针对上面的示例代码,这里假设一个线程A执行writerOne()方法,执行完后线程 B 执行writerTwo()方法,执行完后线程 C 执行reader ()方法,根据普通final域读写规则,操作1和操作3不能重排序,根据引用类型final域的写规则,操作2和操作3也不能重排序,JMM 可以确保读线程 C 至少能看到写线程 A 在构造函数中对 final 引用对象的成员域的写入。而写线程B对数组元素的写入,读线程C可能看的到,也可能看不到。JMM不能保证线程B的写入对读线程C可见,因为写线程B和读线程C之间存在数据竞争,此时的执行结果不可预知。

 

如果想要确保读线程C看到写线程B对数组元素的写入,写线程B和读线程C之间需要使用同步原语(lock或volatile)来确保内存可见性。

 

 避免对象引用在构造函数当中溢出

写final域的重排序规则可以确保:在引用变量为任意线程可见之前,该引用变量指向的对象的final域已经在构造函数中被正确初始化过了即对其他线程可见。这就是在文章开头final关键字带来的可见性实现。

但是要得到这个效果,有一个前提条件:在构造函数内部,不能让这个被构造对象的引用为其他线程可见,也就是对象引用不能在构造函数中“逸出”。

 

public class FinalReferenceEscapeExample {
    final int i;
    int j;
    static FinalReferenceEscapeExample obj;
 
    public FinalReferenceEscapeExample() {
        i = 1;                    // 1
        j = 2;                    // 2
        obj = this;               // 3 避免怎么做!!!对象引用逸出。
    }
 
    public static void writer() {
        new FinalReferenceEscapeExample();
    }
 
    public static void reader() {
        if (obj != null) {        // 4
            int a = obj.i;     // 5
            int b = obj.j;     //6
        }
    }
}
 

 

针对上面的this引用逸出构造函数的示例代码,假设一个线程A执行writer()方法,另一个线程B执行reader()方法。这里的操作3使得对象还未完成构造前引用就为线程B可见。即使这里的操作3是构造函数的最后一步,且即使在程序中操作3排在操作1和操作2后面,执行read()方法的线程B仍然可能无法看到final域以及普通域被初始化后的值,因为这里的操作1和操作2、操作3之间可能被重排序。

 

因此,在构造函数返回前,被构造对象的引用不能为其他线程可见,因为此时的 final 域可能还没有被初始化,如果对象引用提前逸出,将破坏final关键字的语义,也就是说,final关键字将不能保障原有的可见性。

 

分享到:
评论

相关推荐

    java内存模型jmm

    java内存模型jmm

    Java内存模型详解JMM.docx

    JMM的目的之一是为了提供一个跨平台的内存模型,使得Java开发者可以更好地理解Java语言的语言特性和内存相关的内容。JMM的概念包括堆、栈、本机内存、防止内存泄漏等方面。 JSR133是JMM的一部分,它是Java语言规范...

    深入Java内存模型-JMM

    深入Java内存模型-JMM。。。。。。。。。。。。。。。。。。

    深入理解java内存模型

    Java内存模型的抽象 重排序 处理器重排序与内存屏障指令 happens-before 重排序 数据依赖性 as-if-serial 语义 程序顺序规则 重排序对多线程的影响 顺序一致性 数据竞争与顺序一致性保证 顺序一致性内存模型 同步...

    深入理解 Java 内存模型

    深入理解 Java 内存模型,由程晓明编著,深入理解java内存模型JMM

    深入理解Java内存模型.程晓明(带书签文字版).pdf

    Java 内存模型的抽象 4 重排序 6 处理器重排序与内存屏障指令 7 happens-before 10 重排序 13 数据依赖性 13 as-if-serial 语义 13 程序顺序规则 15 重排序对多线程的影响 15 顺序一致性 19 数据竞争与顺序...

    java内存模型JMM(Java Memory Model)1

    由于JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存(有些地方称为栈空间),工作内存是每个线程的私有数据区域,而Java内存模型中规定

    《深入理解JAVA内存模型》PDF

    局部变量(Local variables),方法定义参数(java语言规范称之为formal method parameters)和异常处理器参数(exception handler parameters)不会在线程之间共享,它们不会有内存可见性问题,也不受内存模型的...

    Java内存模型JMM详解

    主要介绍了Java内存模型JMM详解,涉及volatile和监视器锁,final字段,内存屏障等相关内容,具有一定参考价值,需要的朋友可以了解下。

    Java理论与实践:修复Java内存模型1

    关于同步和线程安全的许多底层混淆是Java内存模型的一些难以直觉到的细微差别。本文还介绍了JMM有一些严重的缺点,如果正确地编写并发的类太困难的话,那么许多并发的类不能按预期工作,并且这是平台中的一个缺点。...

    Java内存模型JMM浅析

     并发编程有多种风格,除了CSP(通信顺序进程)、Actor等模型外,大家熟悉的应该是基于线程和锁的共享内存模型了。在多线程编程中,需要注意三类并发问题:  1、原子性  2、可见性  3、重排序  原子性涉及到...

    深入理解Java内存模型

    Agenda: •什么是Java内存模型JMM •内存可见性 •有序性 •指令重排序 •内存屏障 •顺序一致性与Happens-before规则 •volatile, synchronized, 原子变量,锁, final的原理

    学习Java内存模型JMM心得

    主要介绍了学习Java内存模型JMM的心得以及对其原理做了深入的介绍,有兴趣的朋友学习下吧。

    深入理解Java内存模型(经典).rar

    深入理解Java内存模型

    java内存模型的介绍和说明

    介绍java的内存管理方式和特点 1.JMM 简介 2.堆和栈 3.本机内存 4.防止内存泄漏

    JMM(Java内存模型)及Volatile底层实现原理.md

    Java内存模型及Volatile底层实现原理

    Java内存模型案例讲解.docx

    Java线程之间的通信由Java内存模型简称JMM(Java Memory Mode)控制,JMM决定一个线程对共享变量的写入何时对另一个线程可见。从抽象的角度来看,JMM是这样定义线程和主内存之间的抽象关系的:线程之间的共享变量...

    JMM内存模型图

    Java运行时内存模型图

Global site tag (gtag.js) - Google Analytics