前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >final域内存语义

final域内存语义

作者头像
林淮川
发布2021-12-20 16:11:00
3990
发布2021-12-20 16:11:00
举报
文章被收录于专栏:分布式架构分布式架构

- 写final域重排序规则 -

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

  1. 写final 域的重排序规则禁止把final域的写重排序到构造函数之外
    1. JMM禁止编译器把final域的写重排序到构造函数之外
  2. 编译器会在final域的写之后, 构造函数return之前, 插入一个storestore屏蔽。这个屏蔽禁止处理器把final域的写重排序到构造函数之外。

先看一段代码在分析:

代码语言:javascript
复制
public class FinalExample {
  int i; // 普通变量
  final int j; // final变量
  static FinalExample obj;
  public 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 域
  }
}

比如 writer()里obj = new FinalExample(); 包含两个步骤:

  1. 构建一个FinalExample类型的对象;
  2. 把这个对象的引用赋值给引用变量obj。

这时写普通域操作被编译器排序到构造器之外:

  • 线程A写final域的操作,被写final的重排序规则“限定”在构造函数之内,读线程B正确读取了final变量初始化之后的值
  • 写final域的重排序规则可以确保,在对象引用未任意线程可见之前,对象final域已经正确初始化了。而普通域不具有这个保障

- 读final域重排序规则 -

读final域的重排序,在一个线程中,初次读对象引用与初次读对象包含final域, JMM禁止处理器重排序这两个操作(这个规则仅仅针对处理器)。编译器会在读final操作前面插入一个loadload屏障。

初次读对象引用与初次读该对象包含final域,这两个操作之间存在间接依赖关系, 由于编译器遵守间接依赖关系。因此编译器不会重排序这两个操作。大多数处理器也会遵守间接依赖,也不会重排序这两个操作,但少数处理器允许存在间接依赖关系的操作做重排序(alpha处理器), 这个规则专门用来针对这种处理器。

reader()包含3个操作:

  1. 初次读引用变量obj.
  2. 初次读引用变量obj指向对象的普通域j;
  3. 初次读引用变量obj指向对象的final域i;

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

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

- final 域为引用类型 -

对于应用类型, 写final域重排序规则对编译器和处理器增加约束:

在构造函数内对一个final引用的对象的成员域的写入, 与随后在构造函数外把这个被构造对象的引用赋值给一个引用变量。

代码语言:javascript
复制
public class FinalReferenceExample {
  final int[] intArray; // final 是引用类型
  static FinalReferenceExample obj;
  public FinalReferenceExample() { // 构造函数
    intArray = new int[1];
    intArray[0] = 1;
  }
  public static void writeOne(){ // 写线程A执行
    obj = new FinalReferenceExample();
  }
  public static void writeTwo(){ // 写线程B执行
    obj.intArray[0] = 2;
  }
  public static void reader(){ // 读线程C执行
    if(obj != null){
      int temp1 = obj.intArray[0];
    }
  }
}

前提

假设线程A先执行writeOne() ,执行完后线程B执行writeTwo() 或 线程C执行reader() :

  1. final域写入 intArray = new int[1];
  2. 对final域引用的对象的成员域的写入intArray[0] = 1;
  3. 被构造的对象的引用赋值给某个引用变量 obj = new FinalReferenceExample();

结论:1,3 不能重排序, 2,3 不能重排序

说明

线程B和线程C是竞争关系。JMM可以确保线程C至少能看到线程A在final引用对象的成员域的写入,即C至少能看到数组下标0的值为1. 线程B对数据的写入, 线程C 可能看到也可能看不到。JMM不保证线程B的写入对线程C可见,因为写线程B和读线程C之间存在数据竞争。

如果想确保线程C看到些线程对数据元素的写入, 线程B和线程C之间需要使用同步原语(lock或volatile) 来确保内存可见性。

代码语言:javascript
复制
public class FinalReferenceEscapeExample {
  final int i;
  static FinalReferenceEscapeExample obj;
  public FinalReferenceEscapeExample() {
    i = 1; // 1 写final域
    obj = this; // 2 this引用在此"逸出"
  }
  public static void writer() {
    new FinalReferenceEscapeExample();
  }
  public static void reader() {
    if(obj != null){
      int temp = obj.i;
    }
  }
}

说明

线程A执行writer()方法,线程B执行reader方法。这里的操作2 (this引用在此”逸出”) 使得对象还未完成构造前线程B可见,无法看到final域被初始化后的值。因为这里的操作1 (i=1) 和操作2(obj = this) 之间可能被重排序。

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

final语义在处理器中实现

X86处理器不会对写-写操作做重排序, 在X86处理器中,写final域需要的storestore屏障被省略掉。由于X86处理器不会对存在间接依赖关系的操作做重排序。所以在X86处理器中,读final域需要的loadload屏障也会被省略掉。

end: 在X86处理器中 final域的读/写不会插入任何内存屏障

- 作者介绍 -

若水 架构师一枚,现就职于小米小爱开放平台,一个才貌双全的美女码农,平常喜欢总结知识和刷算法题,经常参加LeetCode算法周赛。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2021-11-26,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 川聊架构 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档