回顾前面的Redis文章:
-
**学习并发先看这一介绍篇
**
final关键字在开发中可以说是很常见,但是对于很多人来说,final关键字可能是个熟悉的陌生人的身份,熟悉在于常用,陌生在于它的内存语义
final关键字介绍
final关键字,可以用来修饰类、方法以及变量,表示的是一种不可变性
-
修饰类,表示该类不可继承
-
修饰方法,表示方法不可覆盖
-
修饰变量,表示变量一旦被赋值不可修改
接下来我们分别看下:
- final修饰类的时候,表示类不可继承
当我们新建一个类去继承FinalTest时,发现报错,代表不可继承,去掉final关键字即可:
-
final修饰成员变量的时候,必须要赋初始值(直接赋值或者在构造函数中赋值),并且不可改变
当我们试图去修改这个final的值时:
-
final修饰成员方法的时候,不可被重写
当我们按照上面那种写法写时,发现没有问题,正常继承,此时我们在父类test函数上加上final关键字试下:
发现报错,代表final修饰的函数是不可以被重写的
final关键字修饰的变量会被JVM和Java应用缓存,提高性能,final修饰的变量、方法和类会被JVM底层优化
内存语义
对于 final 域,编译器和处理器要遵守两个重排序规则:
1、在构造函数内对一个 final 域的写入,与随后把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序
**2、**初次读一个包含 final 域的对象的引用,与随后初次读这个 final 域,这两个操作之间不能重排序
写final域的重排序规则
对于第一点,写 final 域的重排序规则禁止把 final 域的写重排序到构造函数之外,这个规则的实现包含下面 2 个方面:
-
JMM 禁止编译器把 final 域的写重排序到构造函数之外。
-
编译器会在 final 域的写之后,构造函数 return 之前,插入一个 StoreStore 屏障。这个屏障禁止处理器把 final 域的写重排序到构造函数之外。
读final域的重排序规则
在一个线程中,初次读对象引用与初次读该对象包含的 final 域,JMM 禁止处理器重排序这两个操作(注意,这个规则仅仅针对处理器)。编译器会在读 final 域操作的前面插入一个 LoadLoad 屏障
举个例子说明:
来源于官网docs.oracle.com/javase/spec…
这个例子定义了一个final变量x和一个普通变量y,在构造函数中赋值,如果此时有两个线程开始分别调用writer()和reader()方法
**结果:**正常是x=3,y=4;不正常是x=3,y=0;
为什么会出现不正常的情况呢?因为final关键字修饰的变量才能确保不会被重排序到构造函数之后,而普通变量就不一定了
所以经过编译器和处理器重排序之后的非正常情况如下所示:
写线程
读线程
1、执行构造函数
2、构造函数给final变量x赋值3,构造函数执行结束
3、将构造对象的引用赋值给引用变量f
4、读取初始化完成的对象,读取该对象中的普通变量y(问题来了)
5、给普通变量y赋值为4
对于空白final变量在构造函数中的初始化代码不可以重排序到构造函数之后,必须在构造函数里面完成初始化,普通变量在不改变单线程运行结果的情况下的初始化可以重排序到构造函数之后
那第二条,初次读包含final域对象的引用和随后初次读这个final域,这两个操作之间不能重排序,什么意思?
部分脑残的处理器会对读对象和读对象中的变量这两个操作进行重排序,重排序之后导致的问题是,读取普通变量时该普通域还未被初始化,所以读取到的数据肯定是不对的啊
JMM对final变量的读取做了限制,必须要先读取包含它的对象,然后再去读取该final变量
很多方面需要自己静下心来体会,建议大家多读读书,读好书
补充
问题:在面试中常问的面试题:final、finally以及finalize区别(虽然我没遇到过)
**回答:**final关键字就不多说了,用来修饰不可变性;finally则是通常和try catch搭配使用,最后一定会执行的一块代码,常被用来进行资源的释放;finalize是object的一个方法,子类可以重写finalize()方法实现对资源的回收,
关于finally,总结一下:
-
return之后还会执行finally,return值暂存栈中,等待finally执行之后再返回
-
若finally中包含return,则覆盖掉try中的return
-
若finally中改变了值,但在finally中并未return这个值,丢失finally中的改变
**问题:**JSR-133为什么要增强final的内存语义
**回答:**在旧的 Java 内存模型中 ,最严重的一个缺陷就是线程可能看到 final 域的值会改变。比如,一个线程当前看到一个整形 final 域的值为 0(还未初始化之前的默认值),过一段时间之后这个线程再去读这个 final 域的值时,却发现值变为了 1(被某个线程初始化之后的值)。最常见的例子就是在旧的 Java 内存模型中,String 的值可能会改变
叨叨
你知道的越多,你不知道的也越多。
建议:看在人家要关注点赞要的比你对象还勤快的份上,你是不是顶不住了,还不满足一波~~(PS:这真的是个不错的公众号哦)
船长,一个努力的、普通的但又不平凡的互联网人