你先看看下面这段话,看看是否能理解。
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不逸出。
感谢你的阅读,希望这篇内容对你有帮助。