Java final原理

Posted 顧棟

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java final原理相关的知识,希望对你有一定的参考价值。

Java final原理

文章目录

final域重排序规则

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

  1. 在构造函数内对一个fianl域的写入,与随后把这个被构造对象的运用赋值给一个引用变量,这两个操作之间不能重排序。
  2. 初次读一个包含final域的对象的引用,与随后初次读这个final域,这两个操作之间不能重排序。

示例代码

public class FinalDemo 
    private int i;                        //普通域
    private final int j;                  //final域
    private static FinalDemo finalDemo;

    public FinalDemo() 
        i = 1;                            // 1. 写普通域
        j = 2;                            // 2. 写final域
    

    public static void writer() 
        finalDemo = new FinalDemo();
    

    public static void reader() 
        FinalDemo demo = finalDemo;       // 3.读对象引用
        int i = demo.i;                   // 4.读普通域
        int j = demo.j;                   // 5.读final域
    

final域为基本类型

写final域的重排序规则

写final域的重排序规则禁止对final域的写重排序到构造函数之外,这个规则的实现主要包含了两个方面:

  • JMM禁止编译器把final域的写重排序到构造函数之外;
  • 编译器会在final域写之后,构造函数执行结束之前,插入一个storestore屏障。这个屏障可以禁止处理器把final域的写重排序到构造函数之外。

writer()方法中只有一行代码finalDemo = new FinalDemo();,这行代码有两个步骤:

  1. 构造一个FinalDemo的对象
  2. 将对象的引用赋值给引用变量finalDemo

假设线程B的读对象引用和读对象的成员域之间没有重排序(这其实是读final域的重排序规则),由于i,j之间没有数据依赖性,普通域(普通变量)i可能会被重排序到构造函数之外那么下图就是是执行顺序中的一种可能情况。

线程A 线程B 构造函数开始执行 写final域:j=2 【StoreStore屏障】 构造函数开始结束 把构造对象的引用赋值给finalDemo 读对象的引用finalDemo 读对象的普通域i 读对象的final域j 写普通域:i=1 线程A 线程B

线程B会错误的读取i的值,为之前的值0,但是读取final值时为正确的值2。

因此,写final域的重排序规则可以确保:在对象引用为任意线程可见之前,对象的final域已经被正确初始化过了,而普通域就不具有这个保障。比如在上例,线程B有可能就是一个未正确初始化的对象finalDemo。

读final域的重排序规则

读final域重排序规则为:在一个线程中,初次读对象引用和初次读该对象包含的final域,JMM会禁止这两个操作的重排序(注意,这个规则仅仅是针对处理器)。编译器会在读final域操作的前面插入一个LoadLoad屏障。实际上,读对象的引用和读该对象的final域存在间接依赖性,一般处理器不会重排序这两个操作。但是有一些处理器(如alpha处理器)会重排序,因此,这条规则就是针对这些处理器而设定的。

read()方法主要包含了三个操作:

  • 初次读引用变量finalDemo;
  • 初次读引用变量finalDemo的普通域i;
  • 初次读引用变量finalDemo的final与j;

假设线程A写过程没有重排序,那么线程A和线程B有一种的可能执行时序为下图:

线程A 线程B 构造函数开始执行 读对象的普通域i 写普通域:i=1 写final域:j=2 【StoreStore屏障】 构造函数开始结束 把构造对象的引用赋值给finalDemo 读对象引用finalDemo 【LoadLoad屏障】 读对象的final域j 线程A 线程B

读对象的普通域被重排序到了读对象引用finalDemo的前面就会出现线程B还未读到对象引用就在读取该对象的普通域变量,这显然是错误的操作。而final域的读操作就“限定”了在读final域变量前已经读到了该对象的引用,从而就可以避免这种情况。

读final域的重排序规则可以确保:在读一个对象的final域之前,一定会先读这个包含这个final域的对象的引用。

final域为引用类型

上面介绍了final域是基础类型的情况,下面介绍final域为引用类型的情况。

public class FinalReferenceDemo 
    final int[] intArrays;                             //final修饰的是引用类型
    private FinalReferenceDemo finalReferenceDemo;

    public FinalReferenceDemo()                       //构造函数
        intArrays = new int[1];                        //1
        intArrays[0] = 1;                              //2
    

    public void writerOne()                           //线程A
        finalReferenceDemo = new FinalReferenceDemo(); //3
    

    public void writerTwo()                           //线程B
        intArrays[0] = 2;                              //4
    

    public void reader()                              //线程C
        if (finalReferenceDemo != null)               //5
            int temp = finalReferenceDemo.arrays[0];   //6
        
    

对final修饰的对象的成员域写操作

对于引用类型,在基础类型的规则之上,final域写针对编译器和处理器重排序增加了这样的约束:在构造函数内对一个final修饰的对象的成员域的写入,与随后在构造函数之外把这个被构造的对象的引用赋给一个引用变量,这两个操作是不能被重排序的。

针对上面的示例程序,线程A执行wirterOne方法,执行完后线程B执行writerTwo方法,然后线程C执行reader方法。下图就以这种执行时序出现的一种情况来讨论

Java 并发工具CountDownLatch和CyclicBarrier 原理解析

Java Review - 并发编程_ 回环屏障CyclicBarrier原理&源码剖析

Java Review - 并发编程_ 回环屏障CyclicBarrier原理&源码剖析

内存屏障

Java Review - 并发编程_ 回环屏障CyclicBarrier原理&源码剖析

Java一些并发类实现原理