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

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

相关推荐
captain3762 小时前
MyBatis(入门5)其他查询操作
java·数据库·spring boot·mybatis
Novproxy_Z2 小时前
商用代理避坑:如何识别商家拿机房 IP 冒充住宅 IP
java·网络·tcp/ip
夕除3 小时前
Redis 过期策略
redis·笔记·面试
文丑颜不良啊3 小时前
LeetCode1481~1500题目解析
java·leetcode
2601_962885723 小时前
quotes 字段全解:涨跌幅、振幅、换手率的口径与单位
java·前端·spring
深蓝AI3 小时前
Python 3.13 的 Queue.shutdown:异步消费者终于能正常收工了
后端
前端精髓3 小时前
调用openrouter的免费API模型
java·前端·javascript
武子康4 小时前
MultiNet6 空闲崩溃:`EXCVADDR=0x8` 后又出现 `CORRUPT HEAP`
人工智能·后端·agent
步行cgn4 小时前
Spring MVC 拦截器 Interceptor 详解:从执行流程到实战应用
java·spring·mvc
文丑颜不良啊4 小时前
LeetCode1381~1400题目解析
java·leetcode