与前面介绍的锁和volatile相比较,对final域的读和写更像是普通的变量访问。对于final域,编译器和处理器要遵守两个重排序规则:

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

  2、初次读一个包含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域的重排序规则

  写final域的重排序规则禁止把final域的写重排序到构造函数之外。这个规则的实现包含下面2个方面:

  ● JMM禁止编译器把final域的写重排序到构造函数之外。

  ● 编译器会在final域的写之后,构造函数return之前,插入一个StoreStore屏障。这个屏障禁止处理器把final域的写重排序到构造函数之外。

  现在让我们分析writer ()方法。writer ()方法只包含一行代码:finalExample = new FinalExample ()。这行代码包含两个步骤:

  1、构造一个FinalExample类型的对象;

  2、把这个对象的引用赋值给引用变量obj。

  假设线程B读对象引用与读对象的成员域之间没有重排序(马上会说明为什么需要这个假设),下图是一种可能的执行时序: