关于final的一些细节,我有话要说——final域重排序规则

「这是我参与11月更文挑战的第27天,活动详情查看:2021最后一次更文挑战」。

茫茫人海千千万万,感谢这一秒你看到这里。希望我的文章对你的有所帮助!

愿你在未来的日子,保持热爱,奔赴山海!!

题记:关于final关键字,它也是我们一个经常用的关键字,可以修饰在类上、或者修饰在变量、方法上,以此看来定义它的一些不可变性!

像我们经常使用的String类中,它便是final来修饰的类,并且它的字符数组也是被final所修饰的。但是一些final的一些细节你真的了解过吗?

从这篇文章开始,带你深入了解final的细节!

👋从内存模型中了解final

👩‍🦼final域重排序规则

对于JMM内存模型来说,它对final域有以下两种重排序规则:

  1. 写:在构造函数内对final域写入,随后将构造函数的引用赋值给一个引用变量,操作不能重排序。

  2. 读:初次读一个包含final域的对象的引用和随后初次写这个final域,不能重排序。

具体我们根据代码演示一边来讲解吧:

代码:

package com.nz.test;

/**
 * 测试JMM内存模型对final域重排序的规则
 */
public class JMMFinalTest {

    // 普通变量
    private int variable;
    // final变量
    private final int variable2;
    private static JMMFinalTest jmmFinalTest;

    // 构造方法中,将普通变量和final变量进行写的操作
    public JMMFinalTest(){
        variable = 1;  // 1. 写普通变量
        variable2 = 2; // 2. 写final变量
    }

    // 模仿一个写操作 --> 假设线程A进行来写操作
    public static void write() {
        // new 当前类对象 --> 并在构造函数中完成赋值操作
        jmmFinalTest = new JMMFinalTest();
    }

    // 模仿一个读操作 --> 假设线程B进行来读操作
    public static void read() {
        // 读操作:
        JMMFinalTest test = jmmFinalTest; // 3. 读对象的引用
        int localVariable = test.variable;
        int localVariable2 = test.variable2;
    }
}
复制代码

写final域重排序规则

final域重排序规则在构造函数内对final域写入,随后将构造函数的引用赋值给一个引用变量,操作不能重排序。代表禁止对final域的初始化操作必须在构造函数中,不能重排序到构造函数之外,这个规则的实现主要包含了两个方面:

  1. JMM内存模型禁止编译器把final域的写重排序到构造函数之外;
  2. 编译器会在final域写入和构造函数return返回之前,插入一个storestore内存屏障。这个内存屏障可以禁止处理器把final域的写重排序到构造函数之外。

我们再来分析write方法,虽然只有一行代码,但他实际上有三个步骤:

  1. 在JVM的堆中申请一块内存空间
  2. 对象进行初始化操作
  3. 将堆中的内存空间的引用地址赋值给一个引用变量jmmFinalTest。

对于普通变量variable来说,它的初始化操作可以被重排序到构造函数之外,即我们的步骤不是本来1-2-3吗,现在可能造成1-3-2这样初始化操作在构造函数返回后了!

而对于final变量variable2来说,它的初始化操作一定在构造函数之内,即1-2-3。

我们来看一个可能发生的图:

对于变量的可见性来说,因为普通变量variable可能会发生重排序的一个现象,读取的值可能会不一样,可能是0或者是1。但是final变量variable2,它读取的值一定是2了,因为有个StoreStore内存屏障来保证与下面的操作进行重排序的操作。

由此可见,写final域的重排序规则可以哪怕保证我们在对象引用为任意线程可见之前,对象的final域已经被正确初始化过了,而普通域就不具有这个保障

读final域重排序规则

初次读一个包含final域的对象的引用和随后初次写这个final域,不能重排序。怎么实现呢?

它其实处理器会在读final域操作的前面插入一个LoadLoad内存屏障。

我们再来分析read方法,他实有三个步骤:

  1. 初次读引用变量jmmFinalTest;
  2. 初次读引用变量jmmFinalTest的普通域变量variable;
  3. 初次读引用变量jmmFinalTest的final域变量variable2;

我们以写操作正常排序的情况,对于读情况可能发生图解:

对于读对象的普通域变量variable可能发生重排序的现象,被重排序到了读对象引用的前面,此时就会出现线程B还未读到对象引用就在读取该对象的普通域变量,这显然是错误的操作。

而对于final域的读操作通过LoadLoad内存屏障保证在读final域变量前已经读到了该对象的引用,从而就可以避免以上情况的发生。

由此可见,读final域的重排序规则可以确保我们在读一个对象的final域之前,一定会先读这个包含这个final域的对象的引用,而普通域就不具有这个保障。

🌸总结

相信各位看官都对final这一个关键字有了一定了解吧,其实额外扩展自己的知识面也是相当有必要滴,不然别人追问你的时候,你会哑口无言,而一旦你自己每天都深入剖析知识点后,你在今后的对答中都会滔滔不绝,绽放光芒的!!!对吧,我们还有一把东西等着我们探索和摸索中!那我们继续期待下一章的final的内容吧!欢迎期待下一章的到来!

让我们也一起加油吧!本人不才,如有什么缺漏、错误的地方,也欢迎各位人才大佬评论中批评指正!当然如果这篇文章确定对你有点小小帮助的话,也请亲切可爱的人才大佬们给个点赞、收藏下吧,一键三连,非常感谢!

学到这里,今天的世界打烊了,晚安!虽然这篇文章完结了,但是我还在,永不完结。我会努力保持写文章。来日方长,何惧车遥马慢!

感谢各位看到这里!愿你韶华不负,青春无悔!