那么引出另一个问题,这个 StoreLoad 屏障是 Volatile Store 之后添加,还是 Volatile Load 之前添加呢?我们来做下这个实验:
首先保留 Volatile Store,将 Volatile Load 改成 Plain Load,即:
测试结果:
从结果中可以看出,仍然保持了 Consensus。再来看保留 Volatile Load,将 Volatile Store 改成 Plain Store:
测试结果:
发现又乱序了。
所以,可以得出结论,这个 StoreLoad 是加在 Volatile 写之后的,在后面的 JVM 底层源码分析我们也能看出来。
7.4 Final 的作用
Java 中,创建对象通过调用类的构造函数实现,我们还可能在构造函数中放一些初始化一些字段的值,例如:
我们可以这样调用构造器创建一个对象:
我们合并这些步骤,用伪代码表示底层实际执行的是:
他们之间,没有任何内存屏障,同时根据语义分析,1 和 5 之间有依赖关系,所以 1 和 5 的前后顺序不能变。1,2,3,4 之间有依赖,所以 1,2,3,4 的前后顺序也不能变。2,3,4 与 5 之间,没有任何关系,他们之间的执行顺序是可能乱序的。如果 5 在 2,3,4 中的任一一步之前执行,那么就会造成我们可能看到构造器还未执行完,x,y,z 还是初始值的情况。测试下:
在 x86 平台的测试结果,你只会看到两个结果,即 -1, -1, -1(代表没看到对象初始化)和 1, 2, 3(看到对象初始化,并且没有乱序),结果如下图所示(AMD64 是一种 x86 的实现):
这是因为,前文我们也提到过类似的, x86 CPU 是比较强一致性的 CPU,这里不会乱序。至于由于 x86 哪种不乱序性质这里才不乱序,我们后面会看到。
还是和前文一样,我们换到不那么强一致性的 CPU (ARM)上执行,这里看到的结果就比较热闹了,如下图所示(aarch64 是一种 ARM 实现):
那我们如何保证看到构造器执行完的结果呢?
用前面的内存屏障设计,我们可以把伪代码的第五步改成 setRelease,即:
前面我们提到过 setRelease 会在前面加上 LoadStore 和 StoreStore 屏障,StoreStore 屏障会防止 2,3,4 与 5 乱序,所以可以避免这个问题,我们来试试看:
再到前面的 aarch64 机器上试一下,结果是:
从结果可以看出,只能看到要么没初始化,要么完整的构造器执行后的结果了。
我们再进一步,其实我们这里只需要 StoreStore 屏障就够了,由此引出了 Java 的 final 关键字:final 其实就是在更新后面紧接着加入 StoreStore 屏障,这样也相当于在构造器结束之前加入 StoreStore 屏障,保证了只要我们能看到对象,对象的构造器一定是执行完了的。测试代码:
我们再进一步,由于伪代码中 2,3,4 是互相依赖的,所以这里我们只要保证 4 先于 5 执行,那么2,3,一定先于 5 执行,也就是我们只需要对 z 设置为 final,从而加 StoreStore 内存屏障,而不是每个都声明为 final,从而多加内存屏障:
然后,我们继续用 aarch64 测试,测试结果依然是对的:
最后我们需要注意,final 仅仅是在更新后面加上 StoreStore 屏障,如果你在构造器过程中,将 this 暴露了出去,那么还是会看到 final 的值没有初始化,我们测试下:
这次我们在 x86 的机器上就能看到 final 没有初始化:
最后,为何这里的示例中 x86 不需要内存屏障就能实现,参考前面的 CPU 图:
x86 本身 Store 与 Store 之间就不会乱序,天然就有保证。
最后给上表格: