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

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

相关推荐
tryxr2 小时前
矩阵的几种基础变换
java·数据结构·算法·矩阵
写后端的胖头鱼2 小时前
如何区分IO/CPU 密集型,线程池核心参数怎么设置
java·线程池·多线程·io密集·cpu密集
Thneonl2 小时前
你的 Pod 为什么 Pending:调度器两阶段
后端·kubernetes
JAVA面经实录9172 小时前
IDEA 开发知识
java·intellij-idea·idea
狂师2 小时前
cloudflared,不需要服务器和公网 IP 的免费内网穿透,一条命令上手
服务器·程序员·开源
于樱花森上飞舞2 小时前
【Redis】哨兵详解
java·开发语言·数据库·redis
zihan5182 小时前
自己做软件日记9/21 得到了一个良好的文件结构框架
后端·flutter·前端框架·android studio
2601_953824613 小时前
基于SSM的水果超市管理系统
java·tomcat·maven
guo_wen_qiang3 小时前
springboot如何自定义一个启动器starter
java·spring boot·intellij-idea