JUC第七讲:关键字final详解

JUC第七讲:关键字final详解

final 关键字看上去简单,但是真正深入理解的人可以说少之又少。本文是JUC第七讲:关键字final详解,将常规的用法简化,提出一些用法和深入的思考。

文章目录

  • JUC第七讲:关键字final详解
    • 1、带着BAT大厂的面试问题去理解final
    • 2、final基础使用
      • 2.1、修饰类
      • 2.2、修饰方法
        • 1、private final
        • 2、final方法是可以被重载的
      • 2.3、修饰参数
      • 2.4、修饰变量
        • 1、所有的final修饰的字段都是编译期常量吗?
        • 2、static final
        • 3、blank final
      • 2.5、final在项目中的使用
    • 3、final域重排序规则
      • 3.1、final域为基本类型
        • 1、写final域重排序规则
        • 2、读final域重排序规则
      • 3.2、final域为引用类型
        • 1、对final修饰的对象的成员域写操作
        • 2、对final修饰的对象的成员域读操作
      • 3.3、关于final重排序的总结
    • 4、final再深入理解
      • 4.1、final的实现原理
      • 4.2、为什么final引用不能从构造函数中“溢出”
      • 4.3、使用 final 的限制条件和局限性
    • Action:看一道面试题,思考一个有趣的现象?
    • 5、参考文章

1、带着BAT大厂的面试问题去理解final

请带着这些问题继续后文,会很大程度上帮助你更好的理解final。

  • 所有的final修饰的字段都是编译期常量吗? 不是
  • 如何理解private所修饰的方法是隐式的final?
  • 说说final类型的类如何拓展? 比如String是final类型,我们想写个MyString复用所有String中方法,同时增加一个新的toMyString()的方法,应该如何做? 组合
  • final方法可以被重载吗? 可以
  • 父类的final方法能不能够被子类重写? 不可以
  • 说说final域重排序规则?
  • 说说final的原理?
  • 使用 final 的限制条件和局限性?

2、final基础使用

2.1、修饰类

当某个类的整体定义为final时,就表明了你不能打算继承该类,而且也不允许别人这么做。即这个类是不能有子类的。

注意:final类中的所有方法都隐式为final,因为无法覆盖他们,所以在final类中给任何方法添加final关键字是没有任何意义的。

这里顺道说说final类型的类如何拓展? 比如String是final类型,我们想写个MyString复用所有String中方法,同时增加一个新的toMyString()的方法,应该如何做?

设计模式中最重要的两种关系,一种是继承/实现;另外一种是组合关系所以当遇到不能用继承的(final修饰的类),应该考虑用组合,如下代码大概写个组合实现的意思:

class MyString{private String innerString;// ...init & other methods// 支持老的方法public int length(){return innerString.length(); // 通过innerString调用老的方法}// 添加新方法public String toMyString(){//...}
}

2.2、修饰方法

常规的使用就不说了,这里说下:

  • private 方法是隐式的final
  • final方法是可以被重载的
1、private final

类中所有private方法都隐式地指定为final的,由于无法取用private方法,所以也就不能覆盖它。可以对private方法增添final关键字,但这样做并没有什么好处。看下下面的例子:

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的,无法被访问到。

2、final方法是可以被重载的

我们知道父类的final方法是不能够被子类重写的,那么final方法可以被重载吗? 答案是可以的,下面代码是正确的。

public class FinalExampleParent {public final void test() {}public final void test(String str) {}
}

2.3、修饰参数

Java允许在参数列表中以声明的方式将参数指明为final,这意味这你无法在方法中更改参数引用所指向的对象。这个特性主要用来向匿名内部类传递数据。

2.4、修饰变量

常规的用法比较简单,这里通过下面三个问题进一步说明。

1、所有的final修饰的字段都是编译期常量吗?

现在来看编译期常量和非编译期常量,如:

public class Test {// 编译期常量final int i = 1;final static int J = 1;final int[] a = {1,2,3,4};// 非编译期常量Random r = new Random();final int k = r.nextInt();public static void main(String[] args) {}
}

k的值由随机数对象决定,所以不是所有的final修饰的字段都是编译期常量,只是k的值在被初始化后无法被更改

2、static final

一个既是static又是final 的字段 只占据一段不能改变的存储空间,它必须在定义的时候进行赋值,否则编译器将不予通过。

import java.util.Random;
public class Test {static Random r = new Random();final int k = r.nextInt(10);static final int k2 = r.nextInt(10); public static void main(String[] args) {Test t1 = new Test();System.out.println("k="+t1.k+" k2="+t1.k2);Test t2 = new Test();System.out.println("k="+t2.k+" k2="+t2.k2);}
}

上面代码某次输出结果:

k=2 k2=7
k=8 k2=7

我们可以发现对于不同的对象k的值是不同的,但是k2的值却是相同的,这是为什么呢? 因为static关键字所修饰的字段并不属于一个对象,而是属于这个类的。也可简单的理解为static final所修饰的字段仅占据内存的一个一份空间,一旦被初始化之后便不会被更改

3、blank final

Java允许生成空白final,也就是说被声明为final但又没有给出定值的字段,但是必须在该字段被使用之前被赋值,这给予我们两种选择:

  • 在定义处进行赋值(这不叫空白final)
  • 构造器中进行赋值,保证了该值在被使用前赋值

这增强了final的灵活性。

看下面代码:

public class Test {final int i1 = 1;final int i2;//空白finalpublic Test() {i2 = 1;}public Test(int x) {this.i2 = x;}
}

可以看到i2的赋值更为灵活。但是请注意,如果字段由static和final修饰,仅能在声明时赋值或声明后在静态代码块中赋值,因为该字段不属于对象,属于这个类

2.5、final在项目中的使用

场景1:在模版方式模式中,默认模版使用final修饰,防止子类重新实现;

场景2:使用final修饰常量、修饰类,防止被修改

3、final域重排序规则

上面我们聊的final使用,应该属于Java基础层面的,当理解这些后我们就真的算是掌握了final吗? 有考虑过final在多线程并发的情况吗? 在java内存模型中我们知道java内存模型为了能让处理器和编译器底层发挥他们的最大优势,对底层的约束就很少,也就是说针对底层来说java内存模型就是一弱内存数据模型。同时,处理器和编译为了性能优化会对指令序列有编译器和处理器重排序。那么,在多线程情况下,final会进行怎样的重排序? 会导致线程安全的问题吗? 下面,就来看看final的重排序。

3.1、final域为基本类型

先看一段示例性的代码:

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()方法。

1、写final域重排序规则

写final域的重排序规则禁止对final域的写重排序到构造函数之外,这个规则的实现主要包含了两个方面:

  • JMM禁止编译器把final域的写重排序到构造函数之外
  • 编译器会在final域写之后,构造函数return之前,插入一个storestore屏障。这个屏障可以禁止处理器把final域的写重排序到构造函数之外。

我们再来分析writer方法,虽然只有一行代码,但实际上做了两件事情:

  • 构造了一个FinalDemo对象;
  • 把这个对象赋值给成员变量 finalDemo。

我们来画下存在的一种可能执行时序图,如下:

  • img

由于a,b之间没有数据依赖性,普通域(普通变量)a 可能会被重排序到构造函数之外,线程B就有可能读到的是普通变量a初始化之前的值(零值),这样就可能出现错误。而final域变量b,根据重排序规则,会禁止final修饰的变量b重排序到构造函数之外,从而b能够正确赋值,线程B就能够读到final变量初始化后的值。

因此,写final域的重排序规则可以确保:在对象引用为任意线程可见之前,对象的final域已经被正确初始化过了,而普通域就不具有这个保障。比如在上例,线程B有可能就是一个未正确初始化的对象finalDemo。

2、读final域重排序规则

读final域重排序规则为:在一个线程中,初次读对象引用和初次读该对象包含的final域,JMM会禁止这两个操作的重排序。(注意,这个规则仅仅是针对处理器),处理器会在读final域操作的前面插入一个LoadLoad屏障。实际上,读对象的引用和读该对象的final域存在间接依赖性,一般处理器不会重排序这两个操作。但是有一些处理器会重排序,因此,这条禁止重排序规则就是针对这些处理器而设定的。

read()方法主要包含了三个操作:

  • 初次读引用变量 finalDemo;
  • 初次读引用变量 finalDemo的普通域a;
  • 初次读引用变量 finalDemo的final域b;

假设线程A写过程没有重排序,那么线程A和线程B有一种的可能执行时序为下图:

  • img

读对象的普通域被重排序到了读对象引用的前面就会出现线程B还未读到对象引用就在读取该对象的普通域变量,这显然是错误的操作。而final域的读操作就“限定”了在读final域变量前已经读到了该对象的引用,从而就可以避免这种情况。

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

3.2、final域为引用类型

我们已经知道了final域是基本数据类型的时候重排序规则是怎么的了? 如果是引用数据类型了? 我们接着继续来探讨。

1、对final修饰的对象的成员域写操作

针对引用数据类型,final域写针对编译器和处理器重排序增加了这样的约束:在构造函数内对一个final修饰的对象的成员域的写入,与随后在构造函数之外把这个被构造的对象的引用赋给一个引用变量,这两个操作是不能被重排序的。注意这里的是“增加”也就说前面对final基本数据类型的重排序规则在这里还是使用。这句话是比较拗口的,下面结合实例来看。

public class FinalReferenceDemo {final int[] arrays;private FinalReferenceDemo finalReferenceDemo;public FinalReferenceDemo() {arrays = new int[1];  //1arrays[0] = 1;        //2}public void writerOne() {finalReferenceDemo = new FinalReferenceDemo(); //3}public void writerTwo() {arrays[0] = 2;  //4}public void reader() {if (finalReferenceDemo != null) {  //5int temp = finalReferenceDemo.arrays[0];  //6}}
}

针对上面的实例程序,线程线程A执行wirterOne方法,执行完后线程B执行writerTwo方法,然后线程C执行reader方法。下图就以这种执行时序出现的一种情况来讨论。

  • img

由于对final域的写禁止重排序到构造方法外,因此1和3不能被重排序。由于一个final域的引用对象的成员域写入不能与随后将这个被构造出来的对象赋给引用变量重排序,因此2和3不能重排序。

2、对final修饰的对象的成员域读操作

JMM可以确保线程C至少能看到写线程A对final引用的对象的成员域的写入,即能看下arrays[0] = 1,而写线程B对数组元素的写入可能看到可能看不到。JMM不保证线程B的写入对线程C可见,线程B和线程C之间存在数据竞争,此时的结果是不可预知的。如果可见的,可使用锁或者volatile。

3.3、关于final重排序的总结

按照final修饰的数据类型分类:

  • 基本数据类型:
    • final域写:禁止final域写与构造方法重排序,即禁止final域写重排序到构造方法之外,从而保证该对象对所有线程可见时,该对象的final域全部已经初始化过。
    • final域读:禁止初次读对象的引用与读该对象包含的final域的重排序。
  • 引用数据类型:
    • 额外增加约束:禁止在构造函数对一个final修饰的对象的成员域的写入与随后将这个被构造的对象的引用赋值给引用变量 重排序

4、final再深入理解

4.1、final的实现原理

上面我们提到过,写final域会要求编译器在final域写之后,构造函数返回前插入一个StoreStore屏障。读final域的重排序规则会要求编译器在读final域的操作前插入一个LoadLoad屏障。

很有意思的是,如果以X86处理为例,X86不会对写-写重排序,所以StoreStore屏障可以省略。由于不会对有间接依赖性的操作重排序,所以在X86处理器中,读final域需要的LoadLoad屏障也会被省略掉。也就是说,以X86为例的话,对final域的读/写的内存屏障都会被省略!具体是否插入还是得看是什么处理器

4.2、为什么final引用不能从构造函数中“溢出”

这里还有一个比较有意思的问题:上面对final域写重排序规则可以确保我们在使用一个对象引用的时候该对象的final域已经在构造函数被初始化过了。但是这里其实是有一个前提条件的,也就是:在构造函数,不能让这个被构造的对象被其他线程可见,也就是说该对象引用不能在构造函数中“溢出”。以下面的例子来说:

public class FinalReferenceEscapeDemo {private final int a;private FinalReferenceEscapeDemo referenceDemo;public FinalReferenceEscapeDemo() {a = 1;  //1referenceDemo = this; //2}public void writer() {new FinalReferenceEscapeDemo();}public void reader() {if (referenceDemo != null) {  //3int temp = referenceDemo.a; //4}}
}

可能的执行时序如图所示:

  • img

假设一个线程A执行writer方法,另一个线程执行reader方法。因为构造函数中操作1和2之间没有数据依赖性,1和2可以重排序,先执行了2,这个时候引用对象referenceDemo是个没有完全初始化的对象,而当线程B去读取该对象时就会出错。尽管依然满足了final域写重排序规则:在引用对象对所有线程可见时,其final域已经完全初始化成功。但是,引用对象“this”逸出,该代码依然存在线程安全的问题。

4.3、使用 final 的限制条件和局限性

当声明一个 final 成员时,必须在构造函数退出前设置它的值。

public class MyClass {private final int myField = 1;public MyClass() {...}
}

或者

public class MyClass {private final int myField;public MyClass() {...myField = 1;...}
}

将指向对象的成员声明为 final 只能将该引用设为不可变的,而非所指的对象。

下面的方法仍然可以修改该 list。

private final List myList = new ArrayList();
myList.add("Hello");

声明为 final 可以保证如下操作不合法

myList = new ArrayList();
myList = someOtherList;

如果一个对象将会在多个线程中访问并且你并没有将其成员声明为 final,则必须提供其他方式保证线程安全。

" 其他方式 " 可以包括声明成员为 volatile,使用 synchronized 或者显式 Lock 控制所有该成员的访问。

Action:看一道面试题,思考一个有趣的现象?

byte b1=1;
byte b2=3;
byte b3=b1+b2; //当程序执行到这一行的时候会出错,因为b1、b2可以自动转换成int类型的变量,运算时java虚拟机对它进行了转换,结果导致把一个int赋值给byte从而出错

如果对b1 b2加上final就不会出错

final byte b1=1;
final byte b2=3;
byte b3=b1+b2; //不会出错,不会被强制类型转换

加了final就相当于强制这个类型不能进行转换,直接相加。而java虚拟机为了指令的统一快捷,对于shortbyte这些变量的相加,是先转换为int再进行操作。而加了final实际就将这个过程强制无法进行。

参考这篇文章:Is addition of byte converts to int because of java language rules or because of jvm?

5、参考文章

  • 《java并发编程的艺术》
  • 《疯狂java讲义》

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.xdnf.cn/news/139720.html

如若内容造成侵权/违法违规/事实不符,请联系一条长河网进行投诉反馈,一经查实,立即删除!

相关文章

光伏发电系统最大功率跟踪控制MATLAB仿真模型(电导增量法+扰动观察法)

微❤关注“电气仔推送”获得资料(专享优惠) 模型介绍: 模型主要包含光伏电池模块、直流升压模块、以及最大功率跟踪控制模块。 扰动观察法( P&O ): 所谓 P&O 就是每过一会给予系统工作电压一个可…

【C++】C++11——可变参数模板和emplace

可变参数模板的定义方式可变参数模板的传值计算可变参数模板参数个数参数包展开方式递归展开参数包逗号表达式展开参数包 emplace插入 可变参数模板是C11新增的最强大的特性之一,它对参数高度泛化,能够让我们创建可以接受可变参数的函数模板和类模板。 在…

【Less-CSS】初识Less,使编写 CSS 变得简洁

初识Less,使编写 CSS 变得简洁 1.Less简述2.LESS 原理及使用方式3.示例4.less语法5.Easy Less插件 作为一门标记性语言,CSS 的语法相对简单,对使用者的要求较低,但同时也带来一些问题:CSS 需要书写大量看似没有逻辑的代…

软件项目测试用例评审

软件项目测试用例评审是确保测试计划的一部分(即测试用例)满足项目质量和要求的关键步骤之一。以下是一个通用的软件项目测试用例评审流程,希望对大家有所帮助。北京木奇移动技术有限公司,专业的软件外包开发公司,欢迎…

Prometheus+Grafana可视化监控【Redis状态】

文章目录 一、安装Docker二、安装Redis数据库(Docker容器方式)三、安装Prometheus四、安装Grafana五、Pronetheus和Grafana相关联六、安装redis_exporter七、Grafana添加Redis监控模板 一、安装Docker 注意:我这里使用之前写好脚本进行安装Docker,如果已…

计网第五章(运输层)(八)(TCP的连接释放)

目录 一、基本概述 二、具体实现 三、经典问题之为什么客户进程不直接进入关闭状态? 四、保活计时器 一、基本概述 上篇博客( 计网第五章(运输层)(七)(TCP的连接建立)&#xff…

Wiki.js - 下一代的开源Wiki软件

简介:在众多开源的Wiki软件中,Wiki.js无疑是一个独特且现代的选择。基于Node.js构建,使用了最新的Web技术,Wiki.js为用户提供了一个美观且功能丰富的界面,同时还保留了强大的扩展性和自定义性。无论你是为个人、团队或…

硬件故障诊断:快速定位问题

🌷🍁 博主猫头虎(🐅🐾)带您 Go to New World✨🍁 🦄 博客首页——🐅🐾猫头虎的博客🎐 🐳 《面试题大全专栏》 🦕 文章图文…

Java IO流实现文件复制

目录 前言 文件复制底层逻辑 代码实现 ​编辑 重点!!! 完整代码 改善思考 前言 Windows文件复制时我们是使用Ctrl C复制Ctrl V粘贴,上一篇文章Java基础入门对存储文件的相关操作 我们学习了Java IO流对文件的读写操作&…

uploadifive上传工具php版使用

uploadifive自带的DEMO文件。 下载地址&#xff1a; http://www.uploadify.com/download/ <!DOCTYPE HTML> <html> <head> <meta http-equiv"Content-Type" content"text/html; charsetutf-8"> <title>UploadiFive Test&…

MQ - 24 Pulsar集群架构设计与实现

文章目录 导图概述集群构建主节点弱 ZooKeeper 实现数据可靠性安全控制传输加密端到端加密身份认证资源鉴权可观测性总结导图 概述 从设计定位上来看,Pulsar 是作为 Kafka 的升级替代品出现的,它主要解决了 Kafka 在集群层面的弹性和规模限制问题。那么现在我们就从集群的角…

STM32实现PMBus从机程序

最近在野火的STM32F103VET6开发板上实现PMBus从机程序&#xff0c;这个程序参考了以下这篇博客的关于使用中断法实现I2C从机程序&#xff1a;STM32设置为I2C从机模式_iic从机_柒壹漆的博客-CSDN博客 &#xff0c;实测这个程序是可以正常运行的&#xff0c;感谢博主的分享&#…

3.wifi开发,网络编程

网络协议栈LwIP WiFi UDP Clinet编程 WiFi UDP Server编程 WiFi TCP Client编程 WiFi TCP Server编程 一。LWIP原理介绍&#xff0c;API介绍&#xff0c;文件结构 1.Lwip支持的协议 2.API 3.文件结构 1.api目录&#xff1a;应用程序接口文件。 2.arch目录&#xff1a;与硬件和…

k8s master 是如何进行pod的调度的

Master 节点将 Pod 调度到指定的 Node 节点的原理 该工作由 kube-scheduler 来完成&#xff0c;整个调度过程通过执行一些列复杂的算法最终为每个 Pod 计算出一个最佳的目标 Node&#xff0c;该过程由 kube-scheduler 进程自动完成。常见的有轮询调度&#xff08;RR&#xff09…

Chrome浏览器删除网站cookies的解决方案

大家好,我是爱编程的喵喵。双985硕士毕业,现担任全栈工程师一职,热衷于将数据思维应用到工作与生活中。从事机器学习以及相关的前后端开发工作。曾在阿里云、科大讯飞、CCF等比赛获得多次Top名次。现为CSDN博客专家、人工智能领域优质创作者。喜欢通过博客创作的方式对所学的…

[论文笔记]P-tuning v2

引言 今天带来第五篇大模型微调论文笔记P-tuning v2: Prompt Tuning Can Be Comparable to Fine-tuning Across Scales and Tasks。 作者首先指出了prompt tuning的一些不足,比如在中等规模的模型上NLU任务表现不好,还不能处理困难的序列标记任务,缺乏统一应用的能力。 然…

安卓系统--翻译手机rom语言 添加多国语言 编译apk 反编译ODEX 工具步骤解析

很多小品牌机型不具备多语言设置。国内大都是中文。要想换为其他语言除非固件支持。例如国际版固件等等。大厂基本都有中文或者英文或者其他语言配置。而小品牌机型只能通过修改rom来达到多语言调用. 工具步骤演示 今天给友友介绍一款工具&#xff0c;可以用来翻译手机rom语言…

LinkedList相较于Arravlist的特点/优化

Arravlist底层是内存空间连续的数组&#xff0c;可以根据下标进行随机访问&#xff0c;效率比较高&#xff0c;因为在根据下标访问某一个元素时&#xff0c;并不是一个一个去查&#xff0c;而是算出来这个下标的地址&#xff0c;直接根据这个地址的指向去获取的&#xff0c;因为…

Android Key/Trust Store研究+ssl证书密钥

前言&#xff1a;软件搞环境涉及到了中间件thal trustzone certificate key&#xff0c;翻译过来是thal信任区域证书密钥 &#xff0c;不明白这是什么&#xff0c;学习一下 ssl证书密钥 SSL密钥是SSL加密通信中的重要组成部分。SSL证书通过加密算法生成&#xff0c;用于保护网…

思科路由器:NAT的基础配置

一直以来&#xff0c;对于华为、H3C、锐捷交换机的命令配置&#xff0c;不断的有朋友留言&#xff0c;三家交换机的配置命令容易弄混&#xff0c;经常在实际项目配置中出错&#xff0c;因此&#xff0c;找几个基础的示例来练练。 R1配置 Router>en Router>enable Rout…