除了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
JMM的目的之一是为了提供一个跨平台的内存模型,使得Java开发者可以更好地理解Java语言的语言特性和内存相关的内容。JMM的概念包括堆、栈、本机内存、防止内存泄漏等方面。 JSR133是JMM的一部分,它是Java语言规范...
深入Java内存模型-JMM。。。。。。。。。。。。。。。。。。
Java内存模型的抽象 重排序 处理器重排序与内存屏障指令 happens-before 重排序 数据依赖性 as-if-serial 语义 程序顺序规则 重排序对多线程的影响 顺序一致性 数据竞争与顺序一致性保证 顺序一致性内存模型 同步...
深入理解 Java 内存模型,由程晓明编著,深入理解java内存模型JMM
Java 内存模型的抽象 4 重排序 6 处理器重排序与内存屏障指令 7 happens-before 10 重排序 13 数据依赖性 13 as-if-serial 语义 13 程序顺序规则 15 重排序对多线程的影响 15 顺序一致性 19 数据竞争与顺序...
由于JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存(有些地方称为栈空间),工作内存是每个线程的私有数据区域,而Java内存模型中规定
局部变量(Local variables),方法定义参数(java语言规范称之为formal method parameters)和异常处理器参数(exception handler parameters)不会在线程之间共享,它们不会有内存可见性问题,也不受内存模型的...
主要介绍了Java内存模型JMM详解,涉及volatile和监视器锁,final字段,内存屏障等相关内容,具有一定参考价值,需要的朋友可以了解下。
关于同步和线程安全的许多底层混淆是Java内存模型的一些难以直觉到的细微差别。本文还介绍了JMM有一些严重的缺点,如果正确地编写并发的类太困难的话,那么许多并发的类不能按预期工作,并且这是平台中的一个缺点。...
并发编程有多种风格,除了CSP(通信顺序进程)、Actor等模型外,大家熟悉的应该是基于线程和锁的共享内存模型了。在多线程编程中,需要注意三类并发问题: 1、原子性 2、可见性 3、重排序 原子性涉及到...
Agenda: •什么是Java内存模型JMM •内存可见性 •有序性 •指令重排序 •内存屏障 •顺序一致性与Happens-before规则 •volatile, synchronized, 原子变量,锁, final的原理
主要介绍了学习Java内存模型JMM的心得以及对其原理做了深入的介绍,有兴趣的朋友学习下吧。
深入理解Java内存模型
介绍java的内存管理方式和特点 1.JMM 简介 2.堆和栈 3.本机内存 4.防止内存泄漏
Java内存模型及Volatile底层实现原理
Java线程之间的通信由Java内存模型简称JMM(Java Memory Mode)控制,JMM决定一个线程对共享变量的写入何时对另一个线程可见。从抽象的角度来看,JMM是这样定义线程和主内存之间的抽象关系的:线程之间的共享变量...
Java运行时内存模型图