关于java final关键字的可见性

你先看看下面这段话,看看是否能理解。

final关键字在并发上的作用(Java内存模型(JMM)层面):只要对象构造过程中this没有逃逸,那么构造器一结束,JMM会对所有final字段做一个冻结动作。此后,任何线程(不管它是否加锁不加锁)只要拿到这个对象的引用,就保证能看到final字段的正确初始值。

是不是有点好奇,我为啥会写上面这段话,毕竟以往很多资料介绍final关键字时,一般都是:修饰变量只能赋一次值,修饰方法不能被子类重写,修饰类不能被继承。

这不是final的全部内容,今天我们就补充一下final关键字的另外一个作用。

首先先说一下什么是逃逸

逃逸,简单讲就是:一个对象还没构造完就被别人拿去用了。

我们可以在Windows 11,用Java 17写段代码来复现出逃逸这个问题。

Java 复制代码
public class ThisEscape {

    // 两个线程共享的静态变量
    static Holder holder;

    static class Holder {
        int x;

        Holder() {
            // 构造器第一行,this就赋值给holder
            holder = this;
            // 模拟构造器里剩下的初始化工作
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
            }
            // x的赋值排在最后
            this.x = 42;
        }
    }

    public static void main(String[] args) throws Exception {
        // 构造线程:创建对象
        Thread writer = new Thread(() -> {
            new Holder();
        });

        // 读线程:等到引用出现,马上去读x
        Thread reader = new Thread(() -> {
            // 引用还没出现,等一等
            while (holder == null) {
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                }
            }
            // 拿到引用了,读x
            System.out.println("during construction: x = " + holder.x);
        });

        writer.start();
        reader.start();
        // 等两个线程都结束
        writer.join();
        reader.join();
        // 此时构造器肯定执行完了,再读一次
        System.out.println("after construction: x = " + holder.x);
    }
}

一个叫Holder的类,里面一个int变量x。构造器一进来就把this赋给了一个静态变量,this在构造器最开始就被交出去了。后面sleep一秒钟,模拟构造器里剩余的初始化工作,最后才给x赋值42。

另外还有一个线程在等这个静态变量出现引用,引用一出现,马上去读x。

本地跑3次,结果如下:

text 复制代码
during construction: x = 0
after construction: x = 42
during construction: x = 0
after construction: x = 42
during construction: x = 0
after construction: x = 42

3次结果都是一样的。构造期间读到的x是0,构造完成后读到的x是42。

整个执行的时间线是这样的:构造线程进入构造器,第一行holder = this把「还没初始化完」的对象引用交了出去。然后sleep,构造器暂停。读线程轮询发现holder不是null了,拿着这个引用去读x。此刻构造器还在sleep里,x的赋值还没执行,int的默认值是0,读到的就是0。一秒后构造器醒来,this.x = 42执行完毕,构造结束,之后再读就是42。

出现x=0的原因就在这里,构造器还没执行完,this就被交出去了,另一个线程拿到的是一个「初始化到一半的对象」。这叫this逸出。

为了能理解开头我说的那段话,还得讲一下冻结这个动作。

JMM会对final字段做的冻结动作

冻结:每个final字段都有一个对应的freeze动作,这个动作发生在给该字段赋值的构造函数的末尾。

套入我们上面的代码,就是:

  • 在构造函数里写了this.x = 42;
  • 构造函数return的那一瞬间,JMM会额外做一个动作:把x = 42这个事实封存起来;
  • 之后,任何线程只要拿到这个对象的引用去读x,看到的必然是42,不可能0,也不可能是别的中间值。

但是这个冻结动作生效的前提是:this不能逸出。

因为freeze发生在构造函数末尾。如果this在中途就被人拿走了,那读线程完全可能在freeze还没发生的时候就去读x,而此时x还是默认值0。

好,讲到这里,可以再次来理解开头说的那句话了。

它的本质其实是想说:final保证的是final字段的跨线程「可见性」,且这种可见性并不需要你去写volatile、也不需要加锁,当然前提是this不逸出。

感谢你的阅读,希望这篇内容对你有帮助。

相关推荐
YangYang9YangYan4 分钟前
2027数学与应用数学转产品经理:面试解释与项目准备
面试·职场和发展·产品经理
starzy199012 分钟前
Flink SlotManager启动流程源码深度剖析:从ResourceManager到Slot分配的完整链路
java·大数据·flink
极光通讯14 分钟前
服务器内存来料检验(IQC)实操:批次追溯、外观判定与上机验证
java·服务器·算法
4SAPI15 分钟前
AI API Gateway平台哪个好?从架构视角看企业多模型网关选型与实践
java·大数据·人工智能·gateway·php
EatFan17 分钟前
2026 后端 AI 工程化:Spring AI 2.0、MCP 协议与 Agent 内嵌如何收进 Java 生产系统
java·人工智能·spring·agent·spring ai·spring boot 3·mcp
JPower_mr.g25 分钟前
SmartCall 音色管理技术解析:基于 SPI 的可扩展音色注册架构
java·开发语言·人工智能·ai·架构·开源
测试开发Kevin26 分钟前
IDEA工程结构解析:项目、模块、库、Facet、Artifact (工件) 概念说明
java·ide·intellij idea
大侠归来28 分钟前
Spring Boot 2.1 → 3.5 迁移推演:从实战出发的完整路线图
java·spring boot·后端
Wang's Blog34 分钟前
Java 中间件之 RabbitMQ 快速入门: SpringAMQP 的 DirectExchange 路由模式
java·中间件·java-rabbitmq
Sweet锦1 小时前
jDCS 开源项目:面向工业现场的 Modbus RTU 数据采集基础框架
java·spring boot·物联网·开源