一文带你搞懂Java-final关键字

引言

阅读《Java并发编程实战》的基础知识篇发现java中的final作用实在是太大了,故结合实例深入剖析final关键字。

基础

修饰类

final修饰类时意味着该类不能被继承,所有方法都将为final,所有在final类中给任何方法添加final是没有任何意义的。

修饰方法

private方法是隐式的final,final方法是可以重载的。

修饰参数

将参数列表中的参数申明为final,意味着无法在方法中更改参数引用所指向的对象。常常用在匿名内部类中。

interface GreetingService {void greet(String message);
}public void executeGreeting(final String name) {GreetingService service = new GreetingService() {@Overridepublic void greet(String message) {name = "hello"; // 报错System.out.println(message + " " + name);}};service.greet("Hello");
}

注意从Java8开始,即使局部变量和参数没有显式地被声明为final,只要它们实际上没有被修改,就可以在匿名内部类或lambda表达式中使用。

修饰变量

编译期常量和非编译期常量

import java.util.Random;public class Test {//编译期常量final int i = 1;//非编译期常量Random random = new Random();final int k = random.nextInt();
}

所以final修饰的字段不都是编译期间常量,如上的k只是在初始化之后无法被更改了。

static final

static final 只占据一段不能改变的存储空间,必须在定义的时候进行赋值。

blank final

允许生成空白final,但是该字段被使用之前需要被赋值,通常在构造器进行赋值。

final修饰引用变量

被final修饰的变量无法被改变引用,但引用变量内部仍然可以修改值,如下。

import java.util.ArrayList;public class Test {private final List<Integer> list = new ArrayList<>();public void add(int num) {list.add(num); //是允许的}public void change(List<Integer> otherList) {list = otherList; //不被允许}
}

指令重排序

final为基本类型

写final域重排序规则
/*** @author hyy (hjlbupt at 163 dot com)*/
public class FinalInitialDemo {private int a;private boolean flag;private FinalInitialDemo demo;public FinalInitialDemo() {a = 1;flag = true;}public void writer() {demo = new FinalInitialDemo();}public void reader() {if (flag) {int i = a * a;if (i == 0) {// On my dev machine, the variable initial always success.// To solve this problem, add final to the `a` field and `flag` field.System.out.println("Fuck! instruction reordering occurred.");}}}@SuppressWarnings("InfiniteLoopStatement")public static void main(String[] args) throws Exception {while (true) {FinalInitialDemo demo = new FinalInitialDemo();Thread threadA = new Thread(demo::writer);Thread threadB = new Thread(demo::reader);threadA.start();threadB.start();threadA.join();threadB.join();}}
}

上述代码存在并发安全问题,writer和reader同时进行,writer线程进行类的初始化,此时JVM可能会进行指令的重排序,将a,flag等变量的初始化赋值重排序到构造函数之外,导致reader读取的a,flag变量是基础变量的初始值即0和false(指令顺序不一定发生,并且需要特定的硬件和JVM环境)。

原理

  • JMM禁止编译器把final域的写重排序到构造函数之外
  • 编译器会在final域写之后,构造函数return之前,插入一个storestore屏障,可以禁止处理器把final域的写重排序到构造函数之外。
读final域重排序规则
/*** @author hyy (hjlbupt at 163 dot com)*/
public class FinalInitialDemo {private int a;private boolean flag;private FinalInitialDemo demo;public FinalInitialDemo() {a = 1;flag = true;}public void writer() {demo = new FinalInitialDemo();}public void reader() {FinalInitialDemo referenceDemo = demo;int a = referenceDemo.a;boolean flag = referenceDemo.flag;}@SuppressWarnings("InfiniteLoopStatement")public static void main(String[] args) throws Exception {while (true) {FinalInitialDemo demo = new FinalInitialDemo();Thread threadA = new Thread(demo::writer);Thread threadB = new Thread(demo::reader);threadA.start();threadB.start();threadA.join();threadB.join();}}
}

观察到reader线程读取FinalInitialDemo的引用,成员变量a和flag。如果reader在未读取到对象的引用时,就在读取对象的普通域变量,这显然是错误的操作。

原理

  • 在读一个对象的final域之前,一定会先读这个包含final域对象的引用
  • 编译器会在读final域操作的前面插入一个loadload屏障,可以禁止处理器读取对象的普通域在读取对象引用之前。

final为引用类型

写final域重排序规则

这里参考了pdai.tech博客中的内容

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}}@SuppressWarnings("InfiniteLoopStatement")public static void main(String[] args) throws Exception {while (true) {FinalReferenceDemo demo = new FinalReferenceDemo();Thread threadA = new Thread(demo::writerOne);Thread threadB = new Thread(demo::writerTwo);Thread threadC = new Thread(demo::reader);threadA.start();threadB.start();threadC.start();threadA.join();threadB.join();threadC.join();}}
}

线程A先执行writerOne方法,线程B执行writerTwo方法,线程C执行reader方法。在构造函数内对一个final修饰的对象的成员域的写入,与随后在构造函数之外把这个被构造的对象的引用赋给一个引用变量,这两个操作是不能被重排序的。

原理

由于对final域的写禁止重排序到构造方法外,因此1和3不能被重排序。由于一个final域的引用对象的成员域写入不能与随后将这个被构造出来的对象赋给引用变量重排序,因此2和3不能重排序(简单来说即是要等构造函数对final域操作完成后才能进行其他操作)。

读final域重排序规则

上述代码只能保证线程C能看到线程A对final引用的对象的成员域的写入,即能看到arrays[0]=1。而线程B对数组元素的写入是否能看到就不确定了(线程B和线程C存在数据竞争)。

防止重排序的前提条件

上述谈到final域初始化和构造函数初始化之间不能发生指令重排序有一个前提条件:该对象的引用不能在构造函数中“逸出”。

This引用逃逸

参考《Java并发编程实战》的内容,作者提到了两个常见的对象逸出情况:

  • 在构造函数中注册事件监听
  • 在构造函数中启动新线程
public class ThisEscape {private final int var;public ThisEscape(EventSource source) {source.registerListener(new EventListener() {public void onEvent(Event e) {doSomething(e); //一旦注册成功则可能触发回调, 导致访问var变量可能是未赋值的变量,// 隐含发布了ThisEscape实例本身.}});// other initial......var = 1;}public void doSomething(Event e) {System.out.println(var);}
}
public class ThisEscape {private final int var;public ThisEscape() {new Thread(new EscapeRunnable()).start();// ...var = 1;}private class EscapeRunnable implements Runnable {@Overridepublic void run() {System.out.println(ThisEscape.this.var);// ThisEscape.this就可以引用外围类对象, 但是此时外围类对象可能还没有构造完成, // 即发生了外围类的this引用的逃逸}}
} 

简单来说,this逃逸就是说在构造函数返回之前其他线程就持有该对象的引用,调用尚未构造完全的对象的方法可能引发错误。

解决方案

public class SafeListener {private final EventListener listener;private SafeListener() {listener = new EventListener() {public void onEvent(Event e) {doSomething(e);}}}public static SafeListener newInstance(EventSource source) {SafeListener safe = new SafeListener();source.registerListener(safe.listener);return safe;}
}

参考资料

  • https://pdai.tech/md/java/thread/java-thread-x-key-final.html
  • https://blog.csdn.net/liuwg1226/article/details/119955371

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Java SE 深入总结:核心概念与实践

Java SE&#xff08;Standard Edition&#xff09;是Java平台的核心&#xff0c;为开发者提供了丰富的API和工具来构建各种桌面和移动应用。本文将深入探讨Java SE的核心概念&#xff0c;并通过代码示例来展示这些概念的应用。 1. 面向对象编程&#xff08;OOP&#xff09; J…

2024-05-29 blue-VH-driver-对外接口的并行调用-设计与思考

摘要: VH的driver的对外接口, 要做到可以并行&#xff0c;也就是两个不同的线程&#xff0c;分别调用&#xff0c;不能互相阻塞。 本文记录对其的思考和设计。 上下文: 2024-05-28 blue-VH-driver-需求分析及问题分析-CSDN博客 2024-05-27 blue-vh-问题点-CSDN博客 2024-05…

Wpf 使用 Prism 实战开发Day28

首页汇总方块点击导航功能 点击首页汇总方块的时候&#xff0c;跳转到对应的数据页面 step1: 在IndexViewModel 中&#xff0c;给TaskBar 里面Target 属性&#xff0c;赋上要跳转的页面 step2: 创建导航事件命令和方法实现 step3: 实现导航的逻辑。通过取到 IRegionManager 的…

免费,Python蓝桥杯等级考试真题--第17级(含答案解析和代码)

Python蓝桥杯等级考试真题–第17级 一、 选择题 答案&#xff1a;B 解析&#xff1a;&#xff08;x-y&#xff09;%25%21&#xff0c;故答案为B。 答案&#xff1a;B 解析&#xff1a;x16&#xff0c;所以i的值为range&#xff08;1,16&#xff09;&#xff0c;取值为1-15&…

OpenMV学习笔记2——颜色识别

目录 一、打开单颜色识别实例代码 二、代码基础部分 三、阈值选择 四、给识别到的颜色画框 五、多颜色识别 一、打开单颜色识别实例代码 如图&#xff0c;双击打开对应文件即可进入实例代码。 二、代码基础部分 # Single Color RGB565 Blob Tracking Example # # This e…

手机拍照扫描成电子版,这三款软件助你轻松搞定!

在数字化时代&#xff0c;将手机拍照的内容快速转换为电子版已经成为许多人日常生活和工作中不可或缺的技能。无论是快速记录文档、合同&#xff0c;还是将纸质照片、笔记转化为电子格式&#xff0c;手机拍照扫描功能都为我们提供了极大的便利。今天&#xff0c;就为大家介绍三…

11.任务状态查询API函数总结

一、任务相关 API 函数预览 二、任务相关 API 函数详解 1. 函数 uxTaskPriorityGet() 此函数用于获取指定任务的任务优先级&#xff0c;若使用此函数&#xff0c;需在 FreeRTOSConfig.h 文件中设 置配置项 INCLUDE_uxTaskPriorityGet 为 1&#xff0c;此函数的函数原型如下所示…

mybatis异常:Invalid bound statement (not found): com.lm.mapper.ArticleMapper.list

现象&#xff1a; 原因&#xff1a; 无效绑定&#xff0c;应该是mybatis最常见的一个异常了&#xff0c;接口与XML文件没绑定。首先&#xff0c;mapper接口并没有实现类&#xff0c;所以框架会通过JDK动态代理代理模式获取接口的代理实现类&#xff0c;进而根据接口全限定类名…

适合多种苛刻环境的惯性测量单元M-G370PDS

全球IMU市场d在汽车和机器人技术进步和不断增长的应用需求&#xff0c;保持着高速增长的趋势&#xff0c;其中航空航天、国防和汽车等行业对高精度、稳定和紧凑的IMU需求尤为强烈&#xff0c;这些行业对精度和可靠性的高要求直接影响了相关技术的发展方向。 爱普生惯性测量单…

一次绕过waf进行xss的经历

今天室友遇到一个好玩的网站&#xff0c;下面是一些尝试绕过Waf进行XSS的记录。首先该网站没有对左右尖号和单双引号做任何过滤或转义。且有未知的waf或者其他阻止恶意访问的手段。 首先我的访问为 login.asp?f1 时候&#xff0c;页面关键源码为 可能是表示登录次数的一个东西…

01_Spring Ioc(详解) + 思维导图

文章目录 一.概念实操Maven父子工程 二. IOC和DI入门案例【重点】1 IOC入门案例【重点】问题导入1.1 门案例思路分析1.2 实现步骤2.1 DI入门案例思路分析2.2 实现步骤2.3 实现代码2.4 图解演示 三、Bean的基础配置问题导入问题导入1 Bean是如何创建的【理解】2 实例化Bean的三种…

【ai】livekit:Agents 4: livekit-plugins-openai和LiveKit Plugins Silero安装与分析

先提高下性能然后本文 继续按照 上一篇【ai】livekit:Agents 3 : pythonsdk和livekit-agent的可编辑模式下的安装构建 livekit-gent的插件。pycharm 工程 配置Microsoft Defender 排除列表 livekit-plugins-openai 本地安装

Tensorflow 2.0 安装过程

第一步&#xff1a;进入国内清华软件网站 anaconda | 镜像站使用帮助 | 清华大学开源软件镜像站 | Tsinghua Open Source Mirroranaconda 使用帮助 | 镜像站使用帮助 | 清华大学开源软件镜像站&#xff0c;致力于为国内和校内用户提供高质量的开源软件镜像、Linux 镜像源服务&…

九章云极DataCanvas公司重磅亮相第七届数字中国建设峰会

近日&#xff0c;由国家发展改革委、国家数据局、国家网信办、科技部、国务院国资委、福建省人民政府共同主办的第七届数字中国建设峰会在福州盛大举行&#xff0c;九章云极DataCanvas公司重磅亮相峰会现场&#xff0c;深度展示智算中心建设核心成果及“算法算力”一体化AI智算…

最简单的安卓模拟器抓包?

安装模拟器抓包似乎是有个绕不开的话题&#xff0c;但是现在普遍的安卓模拟器抓包会遇到以下问题&#xff1a; 1.证书配置繁琐 2.模拟器不兼容软件 3.系统设置繁琐。 前几天写过一次微信小程序如何抓包&#xff0c;现在来讲一下模拟器怎么抓包吧。首先使用的工具还是TangGo测…

c++ auto 关键字比java var关键字使用频率更高的推测

引出 以下都是图一乐的猜测。 我在学习c的过程中&#xff0c;意外的发现了一个事情&#xff0c;感觉c的auto关键字比java的var关键字使用频率高很多。 不知道是不是因为我使用c的时间比较短的原因。 而java方面&#xff0c;我已经有6年的使用经验&#xff0c;确实比较少使用…

ADF: 获取Data Lake Storage上的文件列表并根据文件名删除文件

假设 Data Lake 上有个test的文件夹&#xff0c;有如下文件 目标&#xff1a;使用Azure Data Factory的Pipeline获取这个目录下的文件名列表&#xff0c;并删除掉以"ETC"开头的文件。 步骤&#xff1a; 1. 需要在Linked services中新建一个能连接到Data Lake的连接…

人工智能应用-实验5-BP 神经网络分类手写数据集

文章目录 &#x1f9e1;&#x1f9e1;实验内容&#x1f9e1;&#x1f9e1;&#x1f9e1;&#x1f9e1;代码&#x1f9e1;&#x1f9e1;&#x1f9e1;&#x1f9e1;分析结果&#x1f9e1;&#x1f9e1;&#x1f9e1;&#x1f9e1;实验总结&#x1f9e1;&#x1f9e1; &#x1f9…

windows 安装 使用 nginx

windows 安装 使用 nginx nginx官网下载地址&#xff1a;https://nginx.org/en/download.html 下载稳定版本即可 下载压缩包解压到即可 进入文件夹中&#xff0c;打开命令行窗口&#xff0c;执行启动命令 start nginx.exe验证&#xff08;默认是80端口&#xff09;&#x…

工程项目管理系统的Java实现:高效协同与信息共享

在当今的工程领域&#xff0c;项目管理的高效协同和信息共享是提升工作效率、降低成本的关键。本文将向您介绍一款基于Java技术构建的工程项目管理系统&#xff0c;该系统采用前后端分离的先进技术框架&#xff0c;功能全面&#xff0c;能够满足不同角色的需求&#xff0c;从项…