李林超博客
首页
归档
留言
友链
动态
关于
归档
留言
友链
动态
关于
首页
Java
正文
41.并发编程之final详解
Leefs
2022-11-15 PM
769℃
0条
[TOC] ### 前言 final在Java中是一个保留的关键字,可以声明成员变量、方法、类以及本地变量。一旦将引用声明作final,将**不能改变这个引用**了,编译器会检查代码,如果试图将变量再次初始化的话,编译器会报编译错误。 ### 一、final基本使用 在`Java`中,`final`关键字可以用来修饰**类**、**方法**和**变量(包括成员变量和局部变量)**。 #### 1.1 修饰类 当用`final`修饰一个类时,表明这个类**不能被继承**。最常见是就是String类,任何类都无法继承它。 ```java public final class String implements java.io.Serializable, Comparable
, CharSequence ``` **如果一个类永远不会让他被继承**(子类继承往往可以重写父类的方法和改变父类属性,会带来一定的安全隐患),**就可以用final进行修饰**。 注意,final类中的成员变量可以根据需要设置为final,但是它的所有成员方法会被隐式地指定为final方法。在使用final修饰类的时候,要注意谨慎选择,除非这个类真的在以后不会用来继承或者出于安全的考虑,尽量不要将类设计为final类。 **解决方法** 设计模式中最重要的两种关系,一种是继承/实现;另外一种是组合关系。所以当遇到不能用继承的(final修饰的类),应该考虑用组合。 **如下代码大概写个组合实现的意思**: ```java class MyString{ private String innerString; // ...init & other methods // 支持老的方法 public int length(){ return innerString.length(); // 通过innerString调用老的方法 } // 添加新方法 public String toMyString(){ //... } } ``` #### 1.2 修饰方法 当父类的方法被`final`修饰的时候,**子类不能重写父类的该方法**,比如在`Object`中,`getClass()`方法就是`final`的,我们就不能重写该方法。 ```java public class Object { public final native Class> getClass(); } ``` 如果想**禁止该方法在子类中被重写**的,可以设置该方法为为`final`。 ##### 1.2.1 private final 因为重写的前提是子类可以从父类中继承此方法,如果父类中final修饰的方法同时访问控制权限为private,将会导致子类中不能直接继承到此方法,此时子类中就可以定义相同的方法名和参数。 **示例** ```java public class Base { private void test() { } } public class Son extends Base{ public void test() { } public static void main(String[] args) { Son son = new Son(); Base father = son; //father.test(); } } ``` **说明** Base和Son都有方法test(),但是这并不是一种覆盖,**因为private所修饰的方法是隐式的final**,也就是无法被继承,所以更不用说是覆盖了,在Son中的test()方法不过是属于Son的新成员罢了,Son进行向上转型得到father,但是`father.test()`是不可执行的,因为Base中的test方法是private的,无法被访问到。 ##### 1.2.2 final方法是可以被重载的 父类的final方法是不能够被子类重写的,但是final方法可以被重载。 ```java public class FinalExampleParent { public final void test() { } public final void test(String str) { } } ``` #### 1.3 修饰参数 Java允许在参数列表中以声明的方式将参数指明为final,这意味着无法在方法中更改参数引用所指向的对象。这个特性主要用来向匿名内部类传递数据。 #### 1.4 修饰变量 **final修饰变量表示这个变量一旦赋值就不能修改**。 当final修饰一个基本数据类型时,表示该基本数据类型的值一旦在初始化后便不能发生变化; 如果final修饰一个引用类型时,则在对其初始化之后便不能再让其指向其他对象了,但该**引用所指向的对象的内容是可以发生变化的**,final只保证这个**引用类型变量所引用的地址不会发生改变,即一直引用这个对象,但这个对象属性是可以改变的**。 ##### 1.4.1 成员变量 Java中,成员变量分为**类变量(static修饰)和实例变量**。 **针对这两种类型的变量赋初值的时机是不同的**: + 类变量可以在**声明变量的时候直接赋初值**或者**在静态代码块中给类变量赋初值**; + 实例变量可以**在声明变量的时候给实例变量赋初值**,**在非静态初始化块中**以及**构造器中赋初值**。 因此类变量有**两个时机赋初值**,而实例变量则可以有**三个时机赋初值**。 被final修饰的变量必须在上述时机赋初值,否则编译器会报错。 **总结** + **final修饰的类变量**:必须要在**静态初始化块**中指定初始值或者**声明该类变量时**指定初始值,而且只能在这两个地方之一进行指定,一旦赋值后不能再修改。 + **final修饰的实例变量**:必要要在**非静态初始化块**,**声明该实例变量**或者**在构造器中**指定初始值,而且只能在这三个地方之一进行指定,一旦赋值后不能再修改。 ##### 1.4.2 局部变量 `final`局部变量由程序员进行**显式初始化**,如果`final`局部变量已经进行了初始化则后面就不能再次进行更改,如果`final`变量未进行初始化,可以进行赋值,**当且仅有一次**赋值,一旦赋值之后再次赋值就会出错。 ### 二、final域重排序规则 前面介绍的只是final关键字的**基础用法**。然而在多线程的层面,final也有其自己的**内存语义**。**主要体现在final域的重排序上**,下面来介绍final的重排序规则。 #### 2.1 final域为基本类型 先看一段示例性的代码: ```java public class FinalDemo { private int a; //普通域 private final int b; //final域 private static FinalDemo finalDemo; public FinalDemo() { a = 1; // 1. 写普通域 b = 2; // 2. 写final域 } public static void writer() { finalDemo = new FinalDemo(); } public static void reader() { FinalDemo demo = finalDemo; // 3.读对象引用 int a = demo.a; //4.读普通域 int b = demo.b; //5.读final域 } } ``` 假设线程A在执行writer()方法,随后另一个线程B执行reader()方法。 ##### 2.1.1 写final域重排序规则 写final域的重排序规则禁止对final域的写重排序到构造函数之外,这个规则的实现主要包含了两个方面: - JMM禁止编译器把final域的写重排序到构造函数之外; - 编译器会在final域写之后,构造函数return之前,插入一个`storestore`屏障。这个屏障可以禁止处理器把final域的写重排序到构造函数之外。 我们再来分析writer方法,虽然只有一行代码,但实际上做了两件事情: - 构造了一个`FinalDemo`对象; - 把这个对象赋值给成员变量`finalDemo`。 我们来画下存在的一种可能执行时序图,如下: ![41.并发编程之final详解01.png](https://lilinchao.com/usr/uploads/2022/11/2748831710.png) 由于a,b之间没有数据依赖性,普通域(普通变量)a可能会被重排序到构造函数之外,线程B就有可能读到的是普通变量a初始化之前的值(零值),这样就可能出现错误。而final域变量b,根据重排序规则,会禁止final修饰的变量b重排序到构造函数之外,从而b能够正确赋值,线程B就能够读到final变量初始化后的值。 **因此,写final域的重排序规则可以确保**: 在对象引用为任意线程可见之前,对象的final域已经被正确初始化过了,而普通域就不具有这个保障。比如在上例,线程B有可能就是一个未正确初始化的对象`finalDemo`。 ##### 2.1.2 读final域重排序规则 **读final域重排序规则为**: 在一个线程中,初次读对象引用和初次读该对象包含的final域,JMM会禁止这两个操作的重排序。(注意,这个规则仅仅是针对处理器),处理器会在读final域操作的前面插入一个`LoadLoad`屏障。 实际上,读对象的引用和读该对象的final域存在间接依赖性,一般处理器不会重排序这两个操作。但是有一些处理器会重排序,因此,这条禁止重排序规则就是针对这些处理器而设定的。 **read()方法主要包含了三个操作**: - 初次读引用变量`finalDemo`; - 初次读引用变量`finalDemo`的普通域a; - 初次读引用变量`finalDemo`的final域b; 假设线程A写过程没有重排序,那么线程A和线程B有一种的可能执行时序为下图: ![41.并发编程之final详解02.png](https://lilinchao.com/usr/uploads/2022/11/4274109019.png) 读对象的普通域被重排序到了读对象引用的前面就会出现线程B还未读到对象引用就在读取该对象的普通域变量,这显然是错误的操作。而final域的读操作就“限定”了在读final域变量前已经读到了该对象的引用,从而就可以避免这种情况。 读final域的重排序规则可以确保:在读一个对象的final域之前,一定会先读这个包含这个final域的对象的引用。 #### 2.2 final域为引用类型 ##### 2.2.1 对final修饰的对象的成员域写操作 针对引用数据类型,final域写针对编译器和处理器重排序增加了这样的约束: 在构造函数内对一个final修饰的对象的成员域的写入,与随后在构造函数之外把这个被构造的对象的引用赋给一个引用变量,这两个操作是不能被重排序的。 注意这里的是“增加”也就说前面对final基本数据类型的重排序规则在这里还是适用。 **示例** ```java public class FinalReferenceDemo { final int[] arrays; private FinalReferenceDemo finalReferenceDemo; public FinalReferenceDemo() { arrays = new int[1]; //1 arrays[0] = 1; //2 } public void writerOne() { finalReferenceDemo = new FinalReferenceDemo(); //3 } public void writerTwo() { arrays[0] = 2; //4 } public void reader() { if (finalReferenceDemo != null) { //5 int temp = finalReferenceDemo.arrays[0]; //6 } } } ``` 针对上面的实例程序,线程A执行`wirterOne`方法,执行完后线程B执行`writerTwo`方法,然后线程C执行reader方法。 下图就以这种执行时序出现的一种情况来讨论: ![41.并发编程之final详解03.png](https://lilinchao.com/usr/uploads/2022/11/4223260738.png) 由于对final域的写禁止重排序到构造方法外,因此1和3不能被重排序。由于一个final域的引用对象的成员域写入不能与随后将这个被构造出来的对象赋给引用变量重排序,因此2和3不能重排序。 ##### 2.2.2 对final修饰的对象的成员域读操作 JMM可以确保线程C至少能看到写线程A对final引用的对象的成员域的写入,即能看下arrays[0] = 1,而写线程B对数组元素的写入可能看到可能看不到。JMM不保证线程B的写入对线程C可见,线程B和线程C之间存在数据竞争,此时的结果是不可预知的。如果可见的,可使用锁或者volatile。 #### 2.3 关于final重排序的总结 按照final修饰的数据类型分类: - 基本数据类型: - `final域写`:禁止final域写与构造方法重排序,即禁止final域写重排序到构造方法之外,从而保证该对象对所有线程可见时,该对象的final域全部已经初始化过。 - `final域读`:禁止初次读对象的引用与读该对象包含的final域的重排序。 - 引用数据类型: - `额外增加约束`:禁止在构造函数对一个final修饰的对象的成员域的写入与随后将这个被构造的对象的引用赋值给引用变量 重排序 ### 三、final再深入理解 #### 3.1 final的实现原理 上面我们提到过,写final域会要求编译器在final域写之后,构造函数返回前插入一个`StoreStore`屏障。读final域的重排序规则会要求编译器在读final域的操作前插入一个`LoadLoad`屏障。 很有意思的是,如果以`X86`处理为例,`X86`不会对写-写重排序,所以`StoreStore`屏障可以省略。 由于不会对有间接依赖性的操作重排序,所以在`X86`处理器中,读final域需要的`LoadLoad`屏障也会被省略掉。 也就是说,以`X86`为例的话,对final域的读/写的内存屏障都会被省略! 具体是否插入还是得看是什么处理器。 #### 3.2 为什么final引用不能从构造函数中“溢出” **这里还有一个比较有意思的问题**: 上面对final域写重排序规则可以确保我们在使用一个对象引用的时候该对象的final域已经在构造函数被初始化过了。 但是这里其实是有一个前提条件的,也就是:在构造函数,不能让这个被构造的对象被其他线程可见,也就是说该对象引用不能在构造函数中“溢出”。 **以下面的例子来说**: ```java public class FinalReferenceEscapeDemo { private final int a; private FinalReferenceEscapeDemo referenceDemo; public FinalReferenceEscapeDemo() { a = 1; //1 referenceDemo = this; //2 } public void writer() { new FinalReferenceEscapeDemo(); } public void reader() { if (referenceDemo != null) { //3 int temp = referenceDemo.a; //4 } } } ``` 可能的执行时序如图所示: ![41.并发编程之final详解04.png](https://lilinchao.com/usr/uploads/2022/11/33122023.png) 假设一个线程A执行writer方法另一个线程执行reader方法。 因为构造函数中操作1和2之间没有数据依赖性,1和2可以重排序,先执行了2,这个时候引用对象`referenceDemo`是个没有完全初始化的对象,而当线程B去读取该对象时就会出错。 尽管依然满足了final域写重排序规则:在引用对象对所有线程可见时,其final域已经完全初始化成功。但是,引用对象“this”逸出,该代码依然存在线程安全的问题。 #### 3.3 使用 final 的限制条件和局限性 当声明一个 final 成员时,必须在构造函数退出前设置它的值。 ```java public class MyClass { private final int myField = 1; public MyClass() { ... } } ``` 或者 ```java public class MyClass { private final int myField; public MyClass() { ... myField = 1; ... } } ``` 将指向对象的成员声明为 final 只能将该引用设为不可变的,而非所指的对象。 下面的方法仍然可以修改该 list。 ```java private final List myList = new ArrayList(); myList.add("Hello"); ``` 声明为 final 可以保证如下操作不合法 ```java myList = new ArrayList(); myList = someOtherList; ``` 如果一个对象将会在多个线程中访问并且你并没有将其成员声明为 final,则必须提供其他方式保证线程安全。 " 其他方式 " 可以包括声明成员为 volatile,使用 synchronized 或者显式 Lock 控制所有该成员的访问。 #### 3.4 再思考一个有趣的现象 ```java byte b1=1; byte b2=3; byte b3=b1+b2;//当程序执行到这一行的时候会出错,因为b1、b2可以自动转换成int类型的变量,运算时java虚拟机对它进行了转换,结果导致把一个int赋值给byte-----出错 ``` 如果对b1 b2加上final就不会出错 ```java final byte b1=1; final byte b2=3; byte b3=b1+b2;//不会出错,相信你看了上面的解释就知道原因了 ``` *附参考原文链接地址* *https://pdai.tech/md/java/thread/java-thread-x-key-final.html*
标签:
并发编程
非特殊说明,本博所有文章均为博主原创。
如若转载,请注明出处:
https://lilinchao.com/archives/2578.html
上一篇
40.并发编程之享元模式
下一篇
42.并发编程之自定义线程池
取消回复
评论啦~
提交评论
栏目分类
随笔
2
Java
326
大数据
229
工具
31
其它
25
GO
47
标签云
FileBeat
工具
Python
Shiro
序列化和反序列化
二叉树
Sentinel
Elastisearch
Eclipse
Stream流
散列
MySQL
SpringCloudAlibaba
BurpSuite
Java
数据结构
人工智能
Zookeeper
DataWarehouse
Java编程思想
稀疏数组
JavaScript
Hadoop
锁
Spark RDD
JavaWEB项目搭建
Java阻塞队列
Map
MyBatis-Plus
FastDFS
友情链接
申请
范明明
庄严博客
Mx
陶小桃Blog
虫洞