聊完volatile,就不得不提Java中另一个与并发安全性密切相关的关键字------final。在我们老程序的并发开发中,final常常被用来"固定"共享变量的值,确保其不可修改,从而避免并发场景下的数据错乱。但很多年轻程序员只知道final能修饰类、方法、变量,却不知道它在并发场景下的核心价值------通过严格的重排序规则,保证了final域的初始化安全性,这也是JMM为了平衡一致性和效率,针对不可变变量设计的特殊保障机制。

和volatile类似,final的并发保障能力,也源于JMM对它的重排序限制和内存语义。其中,重排序规则是final实现初始化安全性的核心,尤其是"写final域规则",更是我们实战中避免初始化bug的关键,接下来我们就重点拆解这部分内容,结合实战踩坑经验,把底层逻辑讲透。
一、final
final关键字的重排序规则,核心目的是"保证final域的初始化安全"------确保线程在获取一个包含final域的对象引用时,该final域已经被完全初始化,不会出现"对象引用已可见,但final域还未初始化完成"的诡异场景。这一点,和我们前文聊到的volatile禁止重排序、保证可见性,有着异曲同工之妙,但适用场景和实现方式截然不同。
final的重排序规则主要分为"写final域规则"和"读final域规则",其中写final域规则是基础,也是最容易踩坑的部分,我们先重点拆解这一规则,结合底层屏障和实战案例,把每一个细节都讲明白。
二、final 域规则
写final域规则,本质上是对"final域初始化"和"对象引用赋值"这两个操作的重排序限制,核心是为了避免编译器和CPU对这两个操作进行重排,从而保证final域的初始化先于对象引用可见,这也是final域能保证初始化安全的核心原因。具体规则分为两点,每一点都对应着底层的实现逻辑,也藏着我们老程序踩过的坑。
2.1、构造函数内对 final 域的写,与对象引用赋值给变量的操作不可重排。
这句话看似晦涩,实则是在给final域的初始化"定规矩":在一个对象的构造函数中,对final域的赋值操作,必须在"将该对象的引用赋值给某个变量"之前完成,编译器和CPU不能对这两个操作进行重排序。简单来说,就是"先初始化final域,再让对象可见",避免出现"对象已经被其他线程看到,但final域还没初始化完成"的情况。
早年我刚接触final关键字时,就曾因为忽略了这一规则,写出过存在并发bug的代码。当时我写了一个简单的实体类,用final修饰一个核心属性,在构造函数中对该属性赋值,然后在构造函数结束前,将对象引用赋值给一个全局变量。本以为这样能保证final域的安全性,可在高并发场景下,偶尔会出现其他线程拿到该对象引用后,final域的值还是默认值(比如null、0)的情况,排查了很久才发现,问题出在编译器的重排序上。
原来,在没有重排序限制的情况下,编译器为了提升效率,可能会将"对象引用赋值"操作,重排到"final域赋值"操作之前------也就是说,虽然我们在代码中写的是"先给final域赋值,再给对象引用赋值",但实际执行时,可能会先将对象引用赋值给全局变量,此时final域还未初始化,其他线程拿到这个对象引用后,自然会读到final域的默认值,导致数据错乱。而写final域规则,就彻底禁止了这种重排序,强制要求final域赋值先执行,对象引用赋值后执行,从根源上避免了这种bug。
这里要强调一个关键细节:这一规则只限制"构造函数内的final域写操作"和"对象引用赋值操作"的重排,对于构造函数内的其他普通域写操作,与对象引用赋值的重排,是不做限制的。这也是final域和普通域的核心区别之一------普通域的初始化可能会被重排到对象引用赋值之后,导致其他线程读到未初始化的普通域,而final域则不会,这也是final域能保证初始化安全的核心优势。
2.2、编译器在 final 域写后、构造函数返回前插入 StoreStore 屏障。
如果说第一条规则是"禁止重排"的约定,那这条规则就是"强制落地"的底层保障。我们知道,仅仅依靠编译器的重排序限制,还不足以完全避免重排问题------CPU层面依然可能会进行重排序,这时候就需要通过内存屏障,来强制约束CPU的执行顺序,而StoreStore屏障,就是用来实现这一目的的核心手段。
具体来说,编译器会在构造函数中,对final域的赋值操作完成后、构造函数返回之前,插入一个StoreStore屏障。这个屏障的核心作用是:禁止将屏障之前的"写操作"(包括final域的写操作、构造函数内的其他写操作),重排到屏障之后的"写操作"(主要是对象引用赋值操作)之后。也就是说,StoreStore屏障相当于一道"墙",把final域的写操作"挡"在前面,确保所有屏障之前的写操作都完成后,再执行屏障之后的对象引用赋值操作。
结合我们前文聊到的内存屏障知识,StoreStore屏障的底层逻辑是:强制将屏障之前的写操作结果,刷新到主内存中,并且禁止后续的写操作重排到屏障之前。这样一来,不仅能禁止编译器的重排序,还能禁止CPU的重排序,从底层确保了final域的写操作,一定先于对象引用赋值操作完成,也确保了final域的初始化结果,能及时刷新到主内存,供其他线程读取。
这里还要补充一个实战细节:StoreStore屏障是编译器自动插入的,我们程序员不需要手动操作,但我们需要明确它的作用------它是写final域规则能够落地的底层保障,也是final域能保证初始化安全的关键。如果没有这个屏障,即使编译器不重排,CPU也可能会对两个操作进行重排,依然会出现final域未初始化就被可见的bug。
最后小结
总结一下写final域规则的核心逻辑:通过"禁止final域写与对象引用赋值重排"的约定,加上"插入StoreStore屏障"的底层保障,强制实现"先初始化final域,再让对象可见",从而保证final域的初始化安全性,避免并发场景下其他线程读到未初始化的final域。这一规则,看似简单,却是final关键字在并发场景下的核心价值所在,也是我们老程序在使用final修饰共享变量时,必须牢记的底层逻辑。