`
春花秋月何时了
  • 浏览: 42429 次
  • 性别: 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 Memory Model),是Java虚拟机规范中定义的一个抽象概念,它描述了在多线程环境下,如何保证各个线程对共享数据的一致性视图。JMM的主要目标是定义程序中各个变量的访问规则,以及在...

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

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

    深入理解 Java 内存模型

    Java 内存模型(Java Memory Model,简称 JMM)是 Java 平台中关于线程如何访问共享变量的一套规则,它定义了线程之间的内存可见性、数据一致性以及指令重排序等关键概念,对于多线程编程和并发性能优化至关重要。...

    java内存模型.pdf

    - JSR133:JSR133是Java内存模型的重要修订,旨在修复早期JMM存在的问题,如内存可见性问题,提供更强的内存语义,并提高性能。它引入了对final和synchronized的新规则,以确保正确同步的代码行为明确且直观。 2. ...

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

    Java内存模型(Java Memory Model, JMM)是Java平台中用于规范线程间通信和内存可见性的重要概念,它的目标是确保多线程环境下的正确同步。然而,原始的JMM存在一些严重的缺陷,导致了开发者在理解和实现线程安全时...

    从 CPU 讲起,深入理解 Java 内存模型!.doc

    Java 内存模型(Java Memory Model,JMM)是 Java 平台规范的一部分,它定义了程序中各个线程如何访问和修改共享变量的规则,确保多线程环境下的正确性。JMM 主要是为了处理并发编程中可能出现的数据一致性问题,...

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

    Java内存模型(Java Memory Model, JMM)是Java平台中用于确保多线程程序正确同步的关键部分。在早期版本的Java中,JMM存在一些漏洞,导致开发者在编写并发程序时面临挑战,因为它们不能保证在所有平台上都能正确...

    深入理解Java内存模型(一)共3页.pdf.zip

    Java内存模型,简称JMM(Java Memory Model),是Java虚拟机规范中定义的一个抽象概念,它描述了在多线程环境下,如何在共享内存中读写数据,以及这些操作的可见性和有序性。深入理解Java内存模型对于编写高效、安全...

    java内存模型

    Java内存模型,简称JMM(Java Memory Model),是Java平台中的一个重要概念,它定义了程序中各个线程如何访问共享变量以及对这些变量的操作顺序。理解JMM对于编写高效的并发代码至关重要,因为JMM保证了多线程环境下...

    修复JAVA内存模型1

    Java 内存模型(JMM)是Java编程语言中用于管理多线程环境下共享变量访问的一个规范。它的设计目标是确保在并发环境中,程序的执行结果对于所有平台都是可预测的。JSR 133(Java SE 5.0和6.0中的主要更新)对原始JMM...

    java 内存模型 jsr-133

    Java内存模型(Java Memory Model,简称JMM)是Java编程语言中的一个重要概念,它规定了程序中各种变量(线程共享变量)的访问规则,以及在并发环境中如何解决数据一致性问题。JMM的核心目标是为了确保所有线程能...

    java内存模型与并发技术.ppt

    对于Java来说,JMM定义了线程如何与内存交互,保证了多线程程序的结果可预测且语义一致。 JMM保证了可见性,这意味着当一个线程修改了共享变量的值,这个变化必须同步回主内存,以便其他线程能够看到最新的值。而...

    JSR 133 FAQ 中文版

    - **背景**:自1997年发布以来,Java内存模型(JMM)暴露出一些缺陷,导致程序行为异常,并限制了常见的编译器优化。 - **目标**:JSR 133的目标是重新定义Java内存模型,使之能够更好地支持多线程编程,确保程序的...

    java memory model

    JSR-133对Java内存模型进行了重大改进,不仅加强了volatile变量和final字段的语义,还引入了一系列规范性内容,以提高Java并发编程的可靠性和效率。通过深入理解和应用这些规范,开发者可以更好地编写出高效且可靠的...

    Java理论与实践:修复Java内存模型,第1部分

    活跃了将近三年的JSR133,近期发布了关于如何修复Java内存模型(JavaMemoryModel,JMM)的公开建议。原始JMM中有几个严重缺陷,这导致了一些难度高得惊人的概念语义,这些概念原来被认为很简单,如volatile、final...

    Java内存模型总结(上)

    Java内存模型(JMM)是Java并发编程中的关键概念,它定义了线程如何访问共享变量,以及如何保证线程间的通信正确性。在Java中,线程间通信是通过共享内存模型完成的,这意味着线程共享程序的公共状态,通过读写内存...

Global site tag (gtag.js) - Google Analytics