Scala 2.13集合中新增的release fence为何在无匹配acquire fence时仍能满足需求?
嘿,这个问题问到点子上了——内存屏障的配对逻辑确实容易绕晕,尤其是结合JMM(Java内存模型)的抽象性来看。咱们一步步把它掰明白:
首先得纠正一个常见误解:release fence不是阻止屏障之后的写操作跑到前面,而是给屏障之前的所有内存写操作“上保险”——确保这些写操作的结果,会在屏障完成后,被后续的某个关键写操作(比如你提到的payload_ready)“带着”一起对其他线程可见。
换句话说,release fence的作用是建立一条严格的happens-before链:屏障前的所有读写操作,必须在屏障后的那个“最终写”之前完成,并且这个最终写的可见性会自动传递屏障前所有操作的可见性。
回到Scala不可变集合的场景:builder内部确实做了很多可变操作(比如修改链表节点、更新计数器),但这些操作都是内部私有的,对外暴露的只有最终构建好的不可变集合实例。
这里的关键逻辑是:
- Builder完成所有内部可变操作后,执行release fence;
- 紧接着执行一个“最终写”——比如把构建好的集合实例赋值给对外的共享引用,或者标记某个“就绪状态”为true;
- 其他线程只会在读取到这个最终写的结果后,才会去访问集合的内容。
根据JMM的规则,当线程B读取到那个“就绪状态”为true(或者拿到了非空的集合引用),就自动建立了这样的happens-before关系:线程A的release fence前的所有操作 → release fence → 最终写操作 → 线程B的读取操作。
这种情况下,线程B不需要额外加acquire fence,因为JMM已经通过“最终写-读取”的同步动作,把release fence的可见性传递过来了。
那什么时候非得用acquire fence和release fence配对呢?主要是两种场景:
- 没有明确的“同步触发点”:如果线程B不是通过观察那个被release fence加持的“最终写”来触发读取,而是直接去读取共享内存的内容,这时候就需要在读取前加acquire fence,否则可能看不到线程A的写操作;
- 跨线程的裸内存访问:比如直接操作Unsafe类的内存地址,没有通过volatile变量、锁或者其他JMM认可的同步机制来传递可见性,这时候必须用release+acquire的配对屏障来确保线程间的内存可见性。
举个反例:如果线程A做完release fence后,没有设置任何就绪标记,直接让线程B读取builder的内部状态,那线程B必须加acquire fence才能看到线程A的操作——但这种场景在Scala集合的设计里根本不会出现,因为builder的内部状态对外是完全隐藏的。
你纠结的“release fence之后的写操作被重排至屏障之后(允许)”和“屏障之后的写操作实际发生在屏障之前(禁止)”,核心区别在于:
- release fence禁止的是屏障前的写操作被重排到屏障后的写操作之后——也就是说,屏障前的所有写,必须在屏障后的写之前完成(从内存可见性的角度);
- 屏障后的写操作当然可以在屏障之后执行,甚至可以和其他屏障后的操作重排,但这不会影响前面操作的可见性,因为前面的操作已经被屏障“固定”在最终写之前了。
举个具体的代码例子:
// 操作1:向builder添加元素 // 操作2:更新builder的内部计数器 releaseFence() // 操作3:设置 payload_ready = true
release fence确保操作1、2一定happens-before操作3。所以当其他线程看到操作3的结果(payload_ready为true),就100%能看到操作1和2的结果。而如果操作3之后还有其他写操作,它们可以和操作3重排,但这和前面的操作无关——因为前面的操作已经被屏障锁定在操作3之前了。
内容的提问来源于stack exchange,提问作者Turin

