「这是我参与11月更文挑战的第27天,活动详情查看:2021最后一次更文挑战」。
茫茫人海千千万万,感谢这一秒你看到这里。希望我的文章对你的有所帮助!
愿你在未来的日子,保持热爱,奔赴山海!!
题记:关于
final关键字,它也是我们一个经常用的关键字,可以修饰在类上、或者修饰在变量、方法上,以此看来定义它的一些不可变性!像我们经常使用的String类中,它便是
final来修饰的类,并且它的字符数组也是被final所修饰的。但是一些final的一些细节你真的了解过吗?从这篇文章开始,带你深入了解
final的细节!
👋从内存模型中了解final
👩🦼final域重排序规则
对于JMM内存模型来说,它对final域有以下两种重排序规则:
-
写:在构造函数内对
final域写入,随后将构造函数的引用赋值给一个引用变量,操作不能重排序。 -
读:初次读一个包含
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域的初始化操作必须在构造函数中,不能重排序到构造函数之外,这个规则的实现主要包含了两个方面:
- JMM内存模型禁止编译器把
final域的写重排序到构造函数之外; - 编译器会在
final域写入和构造函数return返回之前,插入一个storestore内存屏障。这个内存屏障可以禁止处理器把final域的写重排序到构造函数之外。
我们再来分析write方法,虽然只有一行代码,但他实际上有三个步骤:
- 在JVM的堆中申请一块内存空间
- 对象进行初始化操作
- 将堆中的内存空间的引用地址赋值给一个引用变量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方法,他实有三个步骤:
- 初次读引用变量jmmFinalTest;
- 初次读引用变量jmmFinalTest的普通域变量variable;
- 初次读引用变量jmmFinalTest的
final域变量variable2;
我们以写操作正常排序的情况,对于读情况可能发生图解:
对于读对象的普通域变量variable可能发生重排序的现象,被重排序到了读对象引用的前面,此时就会出现线程B还未读到对象引用就在读取该对象的普通域变量,这显然是错误的操作。
而对于final域的读操作通过LoadLoad内存屏障保证在读final域变量前已经读到了该对象的引用,从而就可以避免以上情况的发生。
由此可见,读final域的重排序规则可以确保我们在读一个对象的final域之前,一定会先读这个包含这个final域的对象的引用,而普通域就不具有这个保障。
🌸总结
相信各位看官都对final这一个关键字有了一定了解吧,其实额外扩展自己的知识面也是相当有必要滴,不然别人追问你的时候,你会哑口无言,而一旦你自己每天都深入剖析知识点后,你在今后的对答中都会滔滔不绝,绽放光芒的!!!对吧,我们还有一把东西等着我们探索和摸索中!那我们继续期待下一章的final的内容吧!欢迎期待下一章的到来!
让我们也一起加油吧!本人不才,如有什么缺漏、错误的地方,也欢迎各位人才大佬评论中批评指正!当然如果这篇文章确定对你有点小小帮助的话,也请亲切可爱的人才大佬们给个点赞、收藏下吧,一键三连,非常感谢!
学到这里,今天的世界打烊了,晚安!虽然这篇文章完结了,但是我还在,永不完结。我会努力保持写文章。来日方长,何惧车遥马慢!
感谢各位看到这里!愿你韶华不负,青春无悔!




近期评论