10、final 域重排序规则

  • Java 内存模型为了能让 处理器编译器 底层发挥他们的最大优势,对底层的约束就很少
    • 也就是说针对底层来说 Java 内存模型就是一个 弱内存数据模型
    • 同时, 处理器编译器 为了性能优化,会对 指令序列 进行 重排序

一、规则 总结


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

1、写 final 域的重排规则


  • 构造函数内对一个 final 域的写入,与随后把这个被构造对象的引用赋值一个引用变量,这两个操作之间不能重排序
    • 即:禁止 final 域的写入操作 重排序构造函数之外

  • 这个规则的实现包含下面 2 个方面。
    • 1、JMM 禁止 编译器final 域的写入操作 重排序构造函数之外
    • 2、编译器会在 final 域的写入操作之后构造函数 return 之前,插入一个 StoreStore 屏障
      • 这个 屏障 禁止 处理器final 域的写入操作 重排序构造函数之外

2、读 final 域的重排规则


  • 在一个线程中,初次读一个包含 final 域的对象的引用,与随后初次这个 final 域,这两个操作之间不能重排序

  • 这个规则的实现包含下面 1 个方面。
    • 编译器会在 读 final 域操作之前,插入一个 LoadLoad 屏障
      • 这个 屏障 禁止 处理器 重排序这两个操作。

  • 注意:这个规则仅仅针对 处理器
    • 初次包含 final 域的对象的引用随后初次这个 final 域,两个操作间存在间接依赖关系
    • 由于 编译器 遵守 间接依赖关系。因此,编译器 不会重排序这两个操作。
    • 有少数处理器允许对存在间接依赖关系的操作做重排序,这个规则是专门针对这种处理器的。
      • 如:alpha 处理器。

  • 假设线程 A 在执行 writer() 方法,线程 B 执行 reader() 方法。
public class FinalDemo {
    private int a;  // 普通域
    private final int b; // final域
    private static FinalDemo obj;
 
    public FinalDemo() {
        a = 1; // 可以被指令重排序到 StoreStore 后边。
        b = 2;
        // StoreStore
    }
 
    public static void writer() {
        obj = new FinalDemo();
    }
 
    public static void reader() {
        FinalDemo object = obj;
        int a = object.a;	// 可以被指令重排序到 LoadLoad 后边。
        int b = object.b;
        // LoadLoad
    }
}

3、写 final 域 对 引用类型 扩展约束


  • 对于引用类型,写 final 域的重排序规则编译器处理器 增加了如下约束
    • 构造函数内对一个 final 引用的对象成员域的写入,与随后在构造函数外把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序

  • 这个约束的目的:
    • 避免在构造函数中,由于指令重排序,导致 obj 被赋值一个 半初始化对象
      • 从而避免其它线程在 reader() 方法中 得到的 i 的值为 0
  • 这个约束的实现: volatile 关键字
public class FinalReferenceEscapeExample {
    private final int i;
    // 解决方案:在声明 FinalReferenceEscapeExample obj; 前边添加上 volatile 。
        // 防止在构造函数中,由于指令重排序,导致 obj 被赋值一个 半初始化对象。
        // 避免 reader() 方法中 得到的 i 的值为 0 ;
    private FinalReferenceEscapeExample obj;
 
    public FinalReferenceEscapeExample() {
        i = 1;  								// 1 写 final 域
        obj = this; 							// 2 this 引用在此"逸出"
    }

    public static void reader() {
        if (obj != null) {  					// 3
            int temp = obj.i; 					// 4
        }
    }
}

二、final 域为 基本类型


  • 示例代码:
    • 假设线程 A 在执行 writer() 方法,线程 B 执行 reader() 方法。
public class FinalDemo {
    private int a;  // 普通域
    private final int b; // final域
    private static FinalDemo obj;
 
    public FinalDemo() {
        a = 1; // 1.写普通域
        b = 2; // 2.写 final 域
    }
 
    public static void writer() {
        obj = new FinalDemo();
    }
 
    public static void reader() {
        FinalDemo object = obj; // 3.读对象引用
        int a = object.a;    // 4.读普通域
        int b = object.b;    // 5.读 final 域
    }
}

1、写 final 域的重排规则


  • 写 final 域的重排序规则可以确保:
    • 对象引用为任意线程可见之前对象的 final 域已经被正确初始化过了
    • 而普通域不具有这个保障。

  • writer 方法,虽然只有一行代码,但实际上做了两件事情:
    • 1、构造一个 FinalExample 类型的对象。
    • 2、把这个对象的引用赋值给引用变量 obj

  • 假设,线程 B 读对象引用读对象的成员域之间没有重排序,下图是一种可能的执行时序。

  • 写普通域 的操作被编译器重排序到了构造函数之外
    • 因此,读线程 B 错误地读取了普通变量 i 初始化之前的值
  • 写 final 域 的操作,被写 final 域的重排序规则“限定”在了构造函数之内
    • 读线程 B 正确地读取了 final 变量初始化之后的值

2、读 final 域的重排规则


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

  • reader 方法,包含 3 个操作:
    • 1、初次读引用变量 obj。
    • 2、初次读引用变量 obj 指向对象的普通域 j
    • 3、初次读引用变量 obj 指向对象的 final 域 i

  • 假设,写线程 A 没有发生任何重排序,同时程序在不遵守间接依赖处理器 上执行.
    • 下图是一种可能的执行时序

  • 读对象的普通域的操作被处理器重排序读对象引用之前
    • 读普通域时,该还没有被写线程 A 写入,这是一个错误的读取操作
  • 而读 final 域的重排序规则会把读对象 final 域的操作“限定”在读对象引用之后
    • 此时,该 final 域已经被 A 线程初始化过了,这是一个正确的读取操作

三、写 final 域 对 引用类型 的 扩展约束


1、扩展约束的原因


  • 示例代码:
public class FinalReferenceDemo {
    final int[] intArray;					// final 是引用类型
    private FinalReferenceDemo obj;
 
    public FinalReferenceDemo() {			// 构造函数
        intArray = new int[1];  			// 1
        intArray[0] = 1;        			// 2
    }
 
    public void writerOne() {				// 写线程 A 执行
        obj = new FinalReferenceDemo(); 	// 3
    }
 
    public void writerTwo() {				// 写线程 B 执行
        intArray[0] = 2;  					// 4
    }
 
    public void reader() {					// 读线程 C 执行
        if (obj != null) {  				// 5
            int temp = obj.intArray[0];  	// 6
        }
    }
}
  • 假设,首先线程 A 执行 writerOne() 方法。
    * 线程 A 执行完后,线程 B 执行 writerTwo() 方法。
    * 线程 B 执行完后,线程 C 执行 reader() 方法。
    • 下图是一种可能的线程执行时序。

  • 规则的分析:
    • 1 是对 final 域的写入
    • 2 是对这个 final 域引用的对象成员域的写入
    • 3 是把被构造的对象的引用赋值某个引用变量
  • 这里除了前面提到的 1 和 3不能重排序2 和 3不能重排序

  • JMM 可以确保读线程 C 至少能看到写线程 A 在构造函数中对 final 引用对象成员域的写入
    * 即:线程 C 至少能看到数组下标 0 的值为 1。
    • 然而,写线程 B 对数组元素的写入,读线程 C 可能看得到,也可能看不到
  • JMM 不保证线程 B 的写入对读线程 C 可见。因为,写线程 B 和读线程 C 之间存在数据竞争
    * 此时,的执行结果不可预知
    • 如果想要确保读线程 C 看到写线程 B 对数组元素的写入。
      • 写线程 B 和读线程 C 之间需要使用 同步原语(lock 或 volatile)来确保 内存 可见性

2、为什么 final 域所在对象的对象引用不能从构造函数内“溢出”


  • 写 final 域的重排序规则可以确保:
    • 对象引用为任意线程可见之前对象的 final 域已经被正确初始化过了
  • 其实,要得到这个效果,还需要一个保证:
    • 构造函数内部,不能让这个被构造对象的引用被其它线程所见
      • 即:被构造对象的引用不能在构造函数中“逸出”
  • 代码示例:
public class FinalReferenceEscapeExample {
    private final int i;
    // 解决方案:在声明 FinalReferenceEscapeExample obj; 前边添加上 volatile 。
        // 防止在构造函数中,由于指令重排序,导致 obj 被赋值一个 半初始化对象。
        // 避免 reader() 方法中 得到的 i 的值为 0 ;
    private FinalReferenceEscapeExample obj;
 
    public FinalReferenceEscapeExample() {
        i = 1;  								// 1 写 final 域
        obj = this; 							// 2 this 引用在此"逸出"
    }
 
    public static void reader() {
        if (obj != null) {  					// 3
            int temp = obj.i; 					// 4
        }
    }
}

  • 假设一个线程 A 执行 writer() 方法,另一个线程 B 执行 reader() 方法。
    • 这里的操作 2 使得对象还未完成构造就为线程 B 可见
      • 即使这里的操作 2 是构造函数的最后一步,且在程序中操作 2 排在操作 1 后面
        • 执行 read() 方法的线程仍然可能无法看到 final 域被初始化后的值
      • 因为,这里的操作 1 和操作 2 之间可能被重排序

  • 构造函数返回,被构造对象的引用不能被其它线程所见
    • 因为,此时的 final 域可能还没有被初始化
  • 构造函数返回,任意线程都将保证能看到 final 域正确初始化之后的值

四、final 语义在处理器中的实现


  • 以 X86 处理器为例,说明 final 语义在处理器中的具体实现。
  • 写 final 域的重排序规则会要求编译器final 域的写之后构造函数 return 之前插入一个StoreStore 障屏
  • 读 final 域的重排序规则要求编译器读 final 域的操作前面插入一个 LoadLoad 屏障

  • 由于 X86 处理器不会写-写操作重排序
    • 所以,在 X86 处理器中,写 final 域需要的 StoreStore 障屏会被省略掉
  • 同样,由于 X86 处理器不会对存在间接依赖关系的操作做重排序
    • 所以,在 X86 处理器中,读 final 域需要的 LoadLoad 屏障也会被省略掉
  • 也就是说,在 X86 处理器中,final 域的读/写不会插入任何 内存屏障

五、JSR-133 为什么要增强 final 的语义


  • 在旧的 Java 内存模型中,一个最严重的缺陷就是线程可能看到 final 域的值会改变
    • 如:一个线程当前看到一个整型 final 域的值为 0(还未初始化之前的默认值)。
      • 过一段时间之后这个线程再去读这个 final 域的值时,却发现值变为1(被某个线程初始化之后的值)。
    • 最常见的例子就是在旧的 Java 内存模型中,String 的值可能会改变

  • 为了修补这个漏洞,JSR-133 专家组增强了 final 的语义。
  • 通过为 final 域增加重排序规则,可以为 Java 程序员提供初始化安全保证:
    • 只要对象是正确构造的(被构造对象的引用在构造函数中没有“逸出”)。
    • 那么,不需要使用同步(指 lock 和 volatile 的使用)就可以保证任意线程都能看到这个 final 域构造函数被初始化之后的值
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值