Spring框架并不绑定到Java编程语言中,它只是一个框架。因此,通常,您需要将不同线程访问的非最终字段标记为 。归根结底,Spring Bean只不过是一个Java对象,所有语言规则都适用。volatile
final
字段在 Java 编程语言中受到特殊处理。亚历山大·希皮列夫(Alexander Shipilev),甲骨文表演家,就此事写了一篇很棒的文章。简而言之,当构造函数初始化字段时,用于设置字段值的程序集会添加一个额外的内存屏障,以确保任何线程都能正确看到该字段。final
对于非字段,不会创建此类内存屏障。因此,通常,-annotated 方法完全有可能初始化字段,而另一个线程看不到此值,或者更糟糕的是,当构造函数仅部分执行时,可以看到此值。final
@PostConstruct
这是否意味着您始终需要将非最终
字段标记为易失性?
简而言之,是的。如果某个字段可以由不同的线程访问,则可以这样做。不要犯我刚才想到这个问题时所犯的错误(感谢Jk1的更正),并从Java代码的执行顺序的角度来考虑。您可能认为您的Spring应用程序上下文是在单个线程中引导的。这意味着引导线程不会对非易失性字段有问题。因此,您可能会认为,只要在应用程序完全初始化之前不向另一个线程公开应用程序上下文,即调用带注释的方法,一切都是有序的。这样想,您可以假设,只要您在此引导后不更改字段,其他线程就没有机会缓存错误的字段值。
相反,允许编译后的代码对指令进行重新排序,即即使在相关 Bean 向 Java 代码中的另一个线程公开之前调用了 -annotated 方法,这种发生之前的关系也不一定保留在运行时的编译代码中。因此,另一个线程可能总是读取和缓存非字段,而它要么根本没有初始化,要么甚至部分初始化。这可能会引入微妙的错误,不幸的是,Spring文档没有提到这个警告。JMM的这些细节是我个人更喜欢字段和构造函数注入的原因。@PostConstruct
volatile
final
更新:根据另一个问题中的这个答案,在某些情况下,不将字段标记为仍然会产生有效的结果。我对此进行了进一步调查,Spring框架实际上保证了开箱即用的一定程度的事前安全性。看看JLS关于发生之前的关系,它清楚地指出:volatile
监视器上的解锁发生在该监视器上的每次后续锁定之前。
Spring框架利用了这一点。所有豆子都存储在一个地图中,每次从该地图中注册或检索豆子时,Spring都会获取一个特定的监视器。因此,在注册完全初始化的 Bean 后,同一监视器将被解锁,并且在从另一个线程检索同一 Bean 之前,它被锁定。这将强制此另一个线程遵循 Java 代码的执行顺序所反映的“发生之前”关系。因此,如果您引导 Bean 一次,则访问完全初始化的 Bean 的所有线程都将看到此状态,只要它们以规范方式访问 Bean(即通过查询应用程序上下文或自动拧紧来显式检索)。这使得例如 setter 注入或方法的使用即使没有声明字段也是安全的。事实上,您应该避免使用字段,因为它们会为每次读取引入运行时开销,当循环访问字段时,这些开销可能会变得痛苦,并且因为关键字表示错误的意图。(顺便说一句,据我所知,Akka框架应用了类似的策略,除了Spring之外,Akka在这个问题上放了一些线。@PostConstruct
volatile
volatile
但是,此保证仅用于在自举后检索 Bean。如果在非字段引导后更改非字段,或者在 Bean 引用初始化期间泄漏该字段,则此保证不再适用。volatile
查看此较旧的博客文章,其中更详细地描述了此功能。显然,这个功能没有被记录下来,因为即使是Spring的人也知道(但很长一段时间没有做任何事情)。