关于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不逸出。

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

相关推荐
我的xiaodoujiao44 分钟前
Django 基础知识详细图文教程 10-Django 模板引擎 3
后端·python·测试工具·django
拖孩1 小时前
给小程序加「一键发公众号贴图」,我把 wx.shareToOfficialAccount 的三个坑趟了一遍
前端·后端·微信小程序
Wang's Blog1 小时前
Java 项目实战: 外卖平台-MyBatis-Plus分页插件与员工分页查询
java·项目开发
MetaLite1 小时前
全网最好的SpringBoot接口请求对象设计
java·spring boot·后端
蜗牛互联网1 小时前
AI给面试打分不够,求职者更需要可核对的证据
java·人工智能·后端
EatFan1 小时前
gRPC 为什么比 HTTP+JSON 快?详细讲解gRPC
后端·微服务·grpc
此时不提桶,更待何时2 小时前
02-05-B-虚拟线程面试与生产事故实战
java·面试
名字还没想好☜2 小时前
Spring Boot 3.2 RestClient 实战:替代 RestTemplate 调外部接口,超时、连接池与错误处理
java·后端·spring
雪隐2 小时前
16GB 显卡跑 Qwen3.8-27B,只要 7 GB:三进制模型 Bonsai 2 部署手记与双格式实测
人工智能·后端