如果我在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上的线程安全属性,但出于多种原因,我不认为它是足够的保证:

  1. 它只影响单例范围的bean,不能保证其他范围的bean:请求,会话,全局会话,意外暴露的原型范围,自定义用户范围(是的,您可以自己创建一个)。

  2. 它确保对someData写入受到保护,但它不保证读者线程。 可以在这里构建一个等效但简化的示例,其中数据写入是监视器保护器,读取器线程不受此处任何发生之前的关系的影响,并且可以读取过时的数据:

     public class Entity { public Object data; public synchronized void setData(Object data) { this.data = data; } } 
  3. 最后但同样重要的是:我们所讨论的内部监视器是一个实现细节。 没有证件,不保证永久保留,可能会更改,恕不另行通知。

附注:对于以multithreading访问为主题的bean,上述所有内容均属实。 对于原型范围的bean,情况并非如此,除非它们明确地暴露给多个线程,例如通过注入单例范围的bean。