新闻详情

新闻详情

首页 / 资讯中心 / 详情

【Java并发编程系列4】final(内存语义和使用方式)

发布时间:2026/10/1 22:30:04来源:尧图网络
【Java并发编程系列4】final(内存语义和使用方式)
final禁止指令重排分析该部分内容基本摘抄自《深入理解Java内存模型》仅加入自己的总结更详细讲解可以直接参考此书。对final域的读和写更像是普通的变量访问编译器和处理器要遵守两个重排序规则在构造函数内对一个final域的写入与随后把这个被构造对象的引用赋值给一个引用变量这两个操作之间不能重排序。初次读一个包含final域的对象的引用与随后初次读这个final域这两个操作之间不能重排序。下面我们通过一些示例性的代码来分别说明这两个规则public class FinalExample { int i; //普通变量 final int j; //final变量 static FinalExample obj; public void FinalExample () { //构造函数 i 1; //写普通域 j 2; //写final域 } public static void writer () { //写线程A执行 obj new FinalExample (); } public static void reader () { //读线程B执行 FinalExample object obj; //读对象引用 int a object.i; //读普通域 int b object.j; //读final域 } }这里假设一个线程A执行writer ()方法随后另一个线程B执行reader ()方法注意两者的调用先后关系下面我们通过这两个线程的交互来说明这两个规则。写final域的重排序规则写final域的重排序规则禁止把final域的写重排序到构造函数之外。这个规则的实现包含下面2个方面JMM禁止编译器把final域的写重排序到构造函数之外。编译器会在final域的写之后构造函数return之前插入一个StoreStore屏障。这个屏障禁止处理器把final域的写重排序到构造函数之外。现在让我们分析writer ()方法。writer ()方法只包含一行代码finalExample new FinalExample ()。这行代码包含两个步骤构造一个FinalExample类型的对象把这个对象的引用赋值给引用变量obj。假设线程B读对象引用与读对象的成员域之间没有重排序马上会说明为什么需要这个假设下图是一种可能的执行时序在上图中写普通域的操作被编译器重排序到了构造函数之外读线程B错误的读取了普通变量i初始化之前的值。而写final域的操作被写final域的重排序规则“限定”在了构造函数之内读线程B正确的读取了final变量初始化之后的值。写final域的重排序规则可以确保在对象引用为任意线程可见之前对象的final域已经被正确初始化过了而普通域不具有这个保障。以上图为例在读线程B“看到”对象引用obj时很可能obj对象还没有构造完成对普通域i的写操作被重排序到构造函数外此时初始值2还没有写入普通域i。总结一下也就是对象初始化final变量和普通变量然后将初始化的对象引用赋值给其它变量前final变量可以保证已经被初始化但是普通变量不能保证可能会导致读取的普通变量是一个空值或者说是未初始化的值导致异常。读final域的重排序规则读final域的重排序规则如下在一个线程中初次读对象引用与初次读该对象包含的final域JMM禁止处理器重排序这两个操作注意这个规则仅仅针对处理器。编译器会在读final域操作的前面插入一个LoadLoad屏障。reader()方法包含三个操作初次读引用变量obj;初次读引用变量obj指向对象的普通域j。初次读引用变量obj指向对象的final域i。现在我们假设写线程A没有发生任何重排序同时程序在不遵守间接依赖的处理器上执行下面是一种可能的执行时序在上图中读对象的普通域的操作被处理器重排序到读对象引用之前。读普通域时该域还没有被写线程A写入这是一个错误的读取操作。而读final域的重排序规则会把读对象final域的操作“限定”在读对象引用之后此时该final域已经被A线程初始化过了这是一个正确的读取操作。总结一下在读一个对象的final变量之前一定会先读包含这个final域的对象的引用所以不用担心读到对象的final变量会因为重排除导致读到的是一个未初始化的值但是对象的普通变量就不能这样保证。对读和写finlal域整体总结一下写final域的重排序规则会要求译编器在final域的写之后构造函数return之前插入一个StoreStore障屏。读final域的重排序规则要求编译器在读final域的操作前面插入一个LoadLoad屏障。如果final域是引用类型上面我们看到的final域是基础数据类型下面让我们看看如果final域是引用类型将会有什么效果请看下列示例代码public class FinalReferenceExample { final int[] intArray; //final是引用类型 static FinalReferenceExample obj; public FinalReferenceExample () { //构造函数 intArray new int[1]; //1 intArray[0] 1; //2 } public static void writerOne () { //写线程A执行 obj new FinalReferenceExample (); //3 } public static void writerTwo () { //写线程B执行 obj.intArray[0] 2; //4 } public static void reader () { //读线程C执行 if (obj ! null) { //5 int temp1 obj.intArray[0]; //6 } } }在上图中1是对final域的写入2是对这个final域引用的对象的成员域的写入3是把被构造的对象的引用赋值给某个引用变量。这里除了前面提到的1不能和3重排序外2和3也不能重排序。JMM可以确保读线程C至少能看到写线程A在构造函数中对final引用对象的成员域的写入。即C至少能看到数组下标0的值为1。而写线程B对数组元素的写入读线程C可能看的到也可能看不到。JMM不保证线程B的写入对读线程C可见因为写线程B和读线程C之间存在数据竞争此时的执行结果不可预知。如果想要确保读线程C看到写线程B对数组元素的写入写线程B和读线程C之间需要使用同步原语lock或volatile来确保内存可见性。总结一下如果final域为引用类型这个其实和非引用类型禁止重排序的规则基本一样。上面的示例writerTwo()和reader()同时对一个数据进行操作存在竞争关系也很好理解我换一个非引用类型也一样存在并发解决方案就是加锁。为什么final引用不能从构造函数内“逸出”前面我们提到过写final域的重排序规则可以确保在引用变量为任意线程可见之前该引用变量指向的对象的final域已经在构造函数中被正确初始化过了。其实要得到这个效果还需要一个保证在构造函数内部不能让这个被构造对象的引用为其他线程可见也就是对象引用不能在构造函数中“逸出”。为了说明问题让我们来看下面示例代码public class FinalReferenceEscapeExample { final int i; static FinalReferenceEscapeExample obj; public FinalReferenceEscapeExample () { i 1; //1写final域 obj this; //2 this引用在此“逸出” } public static void writer() { new FinalReferenceEscapeExample (); } public static void reader { if (obj ! null) { //3 int temp obj.i; //4 } } }假设一个线程A执行writer()方法另一个线程B执行reader()方法。这里的操作2使得对象还未完成构造前就为线程B可见。即使这里的操作2是构造函数的最后一步且即使在程序中操作2排在操作1后面执行read()方法的线程仍然可能无法看到final域被初始化后的值因为这里的操作1和操作2之间可能被重排序。实际的执行时序可能如下图所示从上图我们可以看出在构造函数返回前被构造对象的引用不能为其他线程可见因为此时的final域可能还没有被初始化。在构造函数返回后任意线程都将保证能看到final域正确初始化之后的值。上面都是八股文也很少会遇到上述使用场景个人也仅作兴趣了解避免踩坑下面总结一下final的常用用法。final关键字有几种用法修饰普通变量注意点用final关键字修饰的变量只能进行一次赋值操作并且在生存期内不可以改变它的值。final修饰的变量可以先声明后赋值。修饰成员变量注意点必须初始化值。被final修饰的成员变量赋值有两种方式1、直接赋值 2、全部在构造方法中赋初值。如果修饰的成员变量是基本类型则表示这个变量的值不能改变。如果修饰的成员变量是一个引用类型则是说这个引用的地址的值不能修改但是这个引用所指向的对象里面的内容还是可以改变的。public class Test { final int age 18; final String name; final String[] hobby; public Test() { this.name lvmenglou; // 正确使用 //this.age 20; // 错误使用 this.hobby new String[4]; // 正确使用 this.hobby[0] movie; // 正确使用 this.hobby[1] sing song; // 正确使用 } }修饰方法注意点被final修饰的方法不能被重写。一个类的private方法会隐式的被指定为final方法。如果父类中有final修饰的方法那么子类不能去重写。修饰类注意点用final去修饰一个类的时候表示这个类不能被继承final类中的成员方法都会被隐式的指定为final方法被final修饰的类final类中的成员变量可以根据自己的实际需要设计为final。在自己设计一个类的时候要想好这个类将来是否会被继承如果可以被继承则该类不能使用final修饰在这里呢一般来说工具类我们往往都会设计成为一个final类。在JDK中被设计为final类的有String、System等。final和static很多时候会容易把static和final关键字混淆static作用于成员变量用来表示只保存一份副本而final的作用是用来保证变量不可变。看下面这个例子class MyClass { public final double i Math.random(); public static double j Math.random(); } public class Test2 { public static void main(String[] args) { MyClass myClass1 new MyClass(); MyClass myClass2 new MyClass(); System.out.println(myClass1.i); System.out.println(myClass1.j); System.out.println(myClass2.i); System.out.println(myClass2.j); } } // 输出 // 0.6885279073145978 // 0.7678464493258529 // 0.5645174724833194 // 0.7678464493258529运行这段代码就会发现每次打印的两个j值都是一样的而i的值却是不同的。从这里就可以知道final和static变量的区别了。总结本章详细讲解了final的内存语义和使用方式内存语义主要是涉及到并发编程相关的知识仅供了解即可。其实我们最终还是需要注重final的使用方式分别从变量、方法、类对齐进行讲解这块知识很简单主要是做个记录最后是final和static也是网上看到的示例也只做个简单的记录。这些内容其实是根据《深入理解Java内存模型》然后结合网上的资料做了相关整理和总结其实都属于Java内存模型方面的内容通过这4篇文章大家对Java内存模型应该会有整体的了解后面才真正讲解并发编程相关的知识。
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

Hi-C数据格式转换:hic、cool、bedpe等五种格式本质解析与验证方法 2026/10/1 23:23:49

Hi-C数据格式转换:hic、cool、bedpe等五种格式本质解析与验证方法

1. 项目概述:为什么HIC数据格式转换不是“点几下就完事”的小事在三维基因组学研究里,“HIC”这个词几乎天天挂在嘴边,但它从来不是单一文件类型——它是一套生态。你刚从Hi-C实验平台拿到原始fastq,下游合作者却要你交cool格式&a…

阅读更多 →
端侧AI算力方案全解析:从NPU选型到RK3588部署实战 2026/10/1 23:23:48

端侧AI算力方案全解析:从NPU选型到RK3588部署实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
异步加载原理与性能优化实战:从首屏白屏到1.2秒的工程实践 2026/10/1 23:23:35

异步加载原理与性能优化实战:从首屏白屏到1.2秒的工程实践

服务器响应用了 800ms,首屏就是等这个 JS 下载完才开始渲染,用户盯着的就是一个白屏。后来我把路由改成动态 import,首屏只加载当前页面需要的代码,瞬间从 3 秒出页面压到了 1.2 秒。这个改动前后不到 10 行代码,但背后…

阅读更多 →
用AI提示词把回归测试从3天压缩到3小时 2026/10/1 23:23:35

用AI提示词把回归测试从3天压缩到3小时

把回归测试从3天压到3小时,不是靠买机器,不是靠换更快的CI,靠的是一套能稳定复用的AI提示词。你可能觉得这个标题有点夸张,先交代一下背景:我们有个核心业务系统,单次发版前要跑四百多条回归用例&#xff0…

阅读更多 →
Selenium驱动的Java新闻爬虫:抓取百度与头条并落库的实战指南 2026/10/1 23:23:35

Selenium驱动的Java新闻爬虫:抓取百度与头条并落库的实战指南

简介:面向 Java 开发者的新闻类爬虫入门资源,适合需要按关键词批量抓取百度新闻、今日头条并入库的初学者。内置 16 个 Java 源文件、1 个 Maven 配置文件和 1 个属性配置文件,构成完整的 HTTP 请求、页面解析、数据存储流程;XML …

阅读更多 →
机器视觉工程师笔记本选型指南:算力密度与工业部署实战 2026/10/1 23:23:35

机器视觉工程师笔记本选型指南:算力密度与工业部署实战

1. 为什么机器视觉工程师的笔记本不能随便买——从一张标定板图像说起我第一次在客户现场调试工业相机时,用的是公司配的那台i5-8250UMX150的轻薄本。跑OpenCV的cv2.findChessboardCorners函数,处理一张20481536的标定图,单帧耗时4.7秒。客户…

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞 ✉