如果我在Spring Framework中的@PostConstruct中初始化它们,我应该将对象属性标记为volatile吗?
假设我在Spring单例bean @PostConstruct
(简化代码)中做了一些初始化:
@Service class SomeService { public Data someData; // not final, not volatile public SomeService() { } @PostConstruct public void init() { someData = new Data(....); } }
我应该担心someData
对其他bean的可见性并将其标记为volatile
吗?
(假设我无法在构造函数中初始化它)
第二种情况:如果我在@PostConstruct
覆盖值(例如在构造函数中进行显式初始化或初始化之后),那么写入@PostConstruct
将不会首先写入此属性?
Spring框架没有绑定到Java编程语言,它只是一个框架。 因此,通常,您需要将不同线程访问的非final字段标记为volatile
。 在一天结束时,Spring bean只不过是一个Java对象,所有语言规则都适用。
final
字段在Java编程语言中得到特殊处理。 甲骨文表演家亚历山大·希普列夫(Alexander Shipilev)就此事写了一篇很棒的文章 。 简而言之,当构造函数初始化final
字段时,用于设置字段值的程序集会添加一个额外的内存屏障,以确保任何线程都能正确地看到该字段。
对于非final
字段,不会创建此类内存屏障。 因此,一般来说,完全有可能@PostConstruct
-annotated方法初始化该字段,并且该值不被另一个线程看到,或者更糟糕的是,当构造函数仅部分执行时看到。
这是否意味着您始终需要将非final
字段标记为易失性?
简而言之,是的。 如果某个字段可以被不同的线程访问,那么就可以。 不要犯这样的错误,只考虑几秒钟的事情(感谢Jk1的纠正),并考虑你的Java代码的执行顺序。 您可能认为您的Spring应用程序上下文是在单个线程中引导的。 这意味着bootstraping线程不会出现非易失性字段的问题。 因此,只要在完全初始化之前不将应用程序上下文暴露给另一个线程,即调用带注释的方法,您可能会认为所有内容都是有序的。 像这样思考,您可以假设,只要您在此引导程序之后不更改字段,其他线程就没有机会缓存错误的字段值。
相反,允许编译的代码重新排序指令,即使在相关bean暴露给Java代码中的另一个线程之前调用了@PostConstruct
-annotated方法,这种情况发生之前 –关系不一定保留在编译中代码在运行时。 因此,另一个线程可能总是读取并缓存非易失volatile
字段,而它尚未完全初始化或甚至部分初始化。 这可能会引入微妙的错误,但遗憾的是,Spring文档没有提到这个警告。 JMM的这些细节是我个人更喜欢final
字段和构造函数注入的原因。
更新 :根据另一个问题的答案 ,有些情况下不将标记字段标记为volatile
仍会产生有效结果。 我对此进行了进一步研究,事实上Spring框架保证了一定程度的安全性– 在开箱即用之前 。 看看JLS关于发生在之前的关系,它明确指出:
监视器上的解锁发生在该监视器上的每个后续锁定之前。
Spring框架使用了这个。 所有bean都存储在一个映射中,每次从这个映射注册或检索bean时,Spring都会获取一个特定的监视器。 结果,在注册完全初始化的bean之后,同一个监视器被解锁,并且在从另一个线程检索相同的bean之前它被锁定。 这会强制其他线程遵守由Java代码的执行顺序反映的之前发生的关系。 因此,如果您引导bean一次,那么访问完全初始化的bean的所有线程都将看到此状态,只要它们以规范方式访问bean(即通过查询应用程序上下文或自动编程进行显式检索)。 这使得例如setter注入或@PostConstruct
方法的使用即使没有声明字段volatile
也是安全的。 事实上,你应该避免使用volatile
字段,因为它们会为每次读取引入运行时开销,这会在循环中访问字段时产生痛苦,并且因为关键字表示错误的意图。 (顺便说一句,据我所知,Akka框架采用了类似的策略,除了Spring之外,Akka 在这个问题上放下了一些线索 。)
但是,这种保证仅用于在引导程序之后检索bean。 如果在引导程序之后更改非易失volatile
字段,或者在初始化期间泄漏bean引用,则此保证不再适用。
查看此较旧的博客条目 ,其中详细介绍了此function。 显然,这个function没有被记录,因为即使Spring人都知道 (但很长一段时间没有做任何事情)。
我是否应该担心someData写入其他bean的可见性并将其标记为volatile?
我没有理由不这样做。 在调用@PostConstruct时,Spring框架不提供额外的线程安全保证,因此可能仍会发生常见的可见性问题。 一种常见的方法是声明someData
final,但是如果你想多次修改该字段,它显然不适合。
如果它是第一次写入该字段应该并不重要。 根据Java Memory Model,重新排序/可见性问题适用于这两种情况。 唯一的例外是最终字段,可以在第一次安全地写入,但后来的分配(例如通过reflection)不能保证可见。
但是, volatile
可以保证其他线程的必要可见性。 它还可以防止部分构造的Data对象不必要的暴露。 由于重新排序问题,可以在完成所有必要对象创建操作之前分配someData
引用,包括构造函数操作和默认值赋值。
更新:根据@raphw Spring的综合研究,将单一豆存储在监视器保护的地图中。 这实际上是正确的,我们可以从org.springframework.beans.factory.support.DefaultSingletonBeanRegistry
的源代码中看到:
public Object getSingleton(String beanName, ObjectFactory singletonFactory) { Assert.notNull(beanName, "'beanName' must not be null"); synchronized (this.singletonObjects) { Object singletonObject = this.singletonObjects.get(beanName); ... return (singletonObject != NULL_OBJECT ? singletonObject : null); } }
这可能会为您提供@PostConstruct
上的线程安全属性,但出于多种原因,我不认为它是足够的保证:
-
它只影响单例范围的bean,不能保证其他范围的bean:请求,会话,全局会话,意外暴露的原型范围,自定义用户范围(是的,您可以自己创建一个)。
-
它确保对
someData
写入受到保护,但它不保证读者线程。 可以在这里构建一个等效但简化的示例,其中数据写入是监视器保护器,读取器线程不受此处任何发生之前的关系的影响,并且可以读取过时的数据:public class Entity { public Object data; public synchronized void setData(Object data) { this.data = data; } }
-
最后但同样重要的是:我们所讨论的内部监视器是一个实现细节。 没有证件,不保证永久保留,可能会更改,恕不另行通知。
附注:对于以multithreading访问为主题的bean,上述所有内容均属实。 对于原型范围的bean,情况并非如此,除非它们明确地暴露给多个线程,例如通过注入单例范围的bean。