假设我在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
将不会首先写到这个属性?
答案 0 :(得分:21)
Spring框架没有绑定到Java编程语言,它只是一个框架。因此,通常,您需要mark a non-final field that is accessed by different threads为volatile
。在一天结束时,Spring bean只不过是一个Java对象,所有语言规则都适用。
final
个字段在Java编程语言中获得特殊处理。 Alexander Shipilev, Oracle表现人,wrote a great article就此事而言。简而言之,当构造函数初始化final
字段时,用于设置字段值的程序集会添加一个额外的内存屏障,以确保任何线程都能正确地看到该字段。
对于非final
字段,不会创建此类内存屏障。因此,一般来说,@PostConstruct
- 带注释的方法很可能初始化字段,而另一个线程看不到这个值,甚至更糟糕的是,当构造函数只是部分执行时才会看到。
这是否意味着您始终需要将非final
字段标记为易失性?
简而言之,是的。如果某个字段可以被不同的线程访问,那么就可以。不要犯这样的错误,只是在思考问题几秒钟时(感谢Jk1的纠正)并根据你的Java代码的执行顺序进行思考。您可能认为您的Spring应用程序上下文是在单个线程中引导的。这意味着bootstraping线程不会出现非易失性字段的问题。因此,只要您没有将应用程序上下文暴露给另一个线程,直到它完全初始化,即调用带注释的方法,您可能会认为所有内容都是有序的。像这样思考,您可以假设,只要您在此引导程序之后不更改字段,其他线程就没有机会缓存错误的字段值。
相反,允许编译的代码重新排序指令,即使在相关bean暴露给Java代码中的另一个线程之前调用@PostConstruct
- 注释方法,此发生在之前关系不一定在运行时保留在已编译的代码中。因此,另一个线程可能始终读取并缓存非volatile
字段,而它尚未初始化或甚至部分初始化。这可能会引入微妙的错误,但遗憾的是,Spring文档并没有提到这个警告。 JMM的这些细节是我个人更喜欢final
字段和构造函数注入的原因。
更新:根据this answer in another question,有些情况下不标记字段,因为volatile
仍会产生有效结果。我对此进行了进一步调查,事实上Spring框架保证了一定量的安全 - 在安全之前开箱即用。看看它明确指出的JLS on happens-before关系:
监视器上的解锁发生在该监视器上的每个后续锁定之前。
Spring框架使用了这个。所有bean都存储在一个映射中,每次从这个映射注册或检索bean时,Spring都会获取一个特定的监视器。结果,在注册完全初始化的bean之后,同一个监视器被解锁,并且在从另一个线程检索相同的bean之前它被锁定。这迫使另一个线程遵守由Java代码的执行顺序反映的发生在之前的关系。因此,如果你引导bean一次,那么访问完全初始化bean的所有线程都将看到这种状态,只要它们以规范方式访问bean(即通过查询应用程序上下文或自动编程进行显式检索)。这使得例如setter注入或使用@PostConstruct
方法即使不声明字段volatile
也是安全的。事实上,您应该避免使用volatile
字段,因为它们会为每次读取引入运行时开销,这会在访问循环中的字段时产生痛苦,并且因为关键字表示错误的意图。 (顺便说一下,据我所知,Akka框架在Akka中应用了类似的策略,除了Spring,drops some lines on the问题。)
但是,此保证仅用于在引导后检索bean。如果在引导之后更改非volatile
字段,或者在初始化期间泄漏bean引用,则此保证不再适用。
查看更详细描述此功能的this older blog entry。显然,这个功能甚至没有被记录为the Spring people are aware of(但长时间没有做任何事情)。
答案 1 :(得分:6)
我是否应该担心someData写入其他bean的可见性并将其标记为volatile?
我认为没有理由不这样做。在调用@PostConstruct时,Spring框架不提供额外的线程安全保证,因此可能仍会发生常见的可见性问题。一种常见的方法是声明someData
final,但是如果你想多次修改该字段,它显然不适合。
如果它是第一次写入该字段应该没关系。根据Java Memory Model,重新排序/可见性问题适用于这两种情况。唯一的例外是最终字段,可以在第一次安全地写入,但后来的分配(例如通过反射)不能保证可见。
但是, 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;
}
}
最后但并非最不重要:我们所讨论的内部监视器是一个实现细节。没有证件,不能保证永久保留,如有更改,恕不另行通知。
附注:以上所述对于多线程访问的bean来说都是如此。对于原型范围的bean,情况并非如此,除非它们明确地暴露给多个线程,例如通过注入单一范围的bean。
答案 2 :(得分:-1)
问题中定义的SomeService
类是不是线程安全的。为了使 class 具有线程安全性,您需要:
someData
字段volatile
。Data
类是线程安全的。SomeService
的实例:https://www.ibm.com/developerworks/library/j-jtp0618/index.html 如果someData
字段不是volatile
,但满足条件2和3,则代码(与 class 相反) ),可以仍然是线程安全的,但前提是必须满足 class 不能控制的其他条件。最安全的是使 class 线程安全。然后,您可以在任何情况下使用 class ,这样会保证始终保持线程安全。 / p>