如果我在Spring Framework的@PostConstruct中初始化对象属性,我应该将它们标记为易失性吗?

2022-09-01 17:23:45

假设,我在Spring singleton bean(简化代码)中做了一些初始化:@PostConstruct

@Service
class SomeService {
  public Data someData; // not final, not volatile

  public SomeService() { }

  @PostConstruct
  public void init() {
     someData = new Data(....);
  }
}

我应该担心其他豆子的可见性并标记它吗?someDatavolatile

(假设我无法在构造函数中初始化它)

第二种情况:如果我覆盖了 中的值(例如在构造函数中显式初始化或初始化之后),那么写进去不会先写到这个属性?@PostConstruct@PostConstruct


答案 1

Spring框架并不绑定到Java编程语言中,它只是一个框架。因此,通常,您需要将不同线程访问的非最终字段标记为 。归根结底,Spring Bean只不过是一个Java对象,所有语言规则都适用。volatile

final字段在 Java 编程语言中受到特殊处理。亚历山大·希皮列夫(Alexander Shipilev),甲骨文表演家就此事写了一篇很棒的文章。简而言之,当构造函数初始化字段时,用于设置字段值的程序集会添加一个额外的内存屏障,以确保任何线程都能正确看到该字段。final

对于非字段,不会创建此类内存屏障。因此,通常,-annotated 方法完全有可能初始化字段,而另一个线程看不到此值,或者更糟糕的是,当构造函数仅部分执行时,可以看到此值。final@PostConstruct

这是否意味着您始终需要将非最终字段标记为易失性?

简而言之,是的。如果某个字段可以由不同的线程访问,则可以这样做。不要犯我刚才想到这个问题时所犯的错误(感谢Jk1的更正),并从Java代码的执行顺序的角度来考虑。您可能认为您的Spring应用程序上下文是在单个线程中引导的。这意味着引导线程不会对非易失性字段有问题。因此,您可能会认为,只要在应用程序完全初始化之前不向另一个线程公开应用程序上下文,即调用带注释的方法,一切都是有序的。这样想,您可以假设,只要您在此引导后不更改字段,其他线程就没有机会缓存错误的字段值。

相反,允许编译后的代码对指令进行重新排序,即即使在相关 Bean 向 Java 代码中的另一个线程公开之前调用了 -annotated 方法,这种发生之前的关系也不一定保留在运行时的编译代码中。因此,另一个线程可能总是读取和缓存非字段,而它要么根本没有初始化,要么甚至部分初始化。这可能会引入微妙的错误,不幸的是,Spring文档没有提到这个警告。JMM的这些细节是我个人更喜欢字段和构造函数注入的原因。@PostConstructvolatilefinal

更新:根据另一个问题中的这个答案,在某些情况下,不将字段标记为仍然会产生有效的结果。我对此进行了进一步调查,Spring框架实际上保证了开箱即用的一定程度的事前安全性。看看JLS关于发生之前的关系,它清楚地指出:volatile

监视器上的解锁发生在该监视器上的每次后续锁定之前。

Spring框架利用了这一点。所有豆子都存储在一个地图中,每次从该地图中注册或检索豆子时,Spring都会获取一个特定的监视器。因此,在注册完全初始化的 Bean 后,同一监视器将被解锁,并且在从另一个线程检索同一 Bean 之前,它被锁定。这将强制此另一个线程遵循 Java 代码的执行顺序所反映的“发生之前”关系。因此,如果您引导 Bean 一次,则访问完全初始化的 Bean 的所有线程都将看到此状态,只要它们以规范方式访问 Bean(即通过查询应用程序上下文或自动拧紧来显式检索)。这使得例如 setter 注入或方法的使用即使没有声明字段也是安全的。事实上,您应该避免使用字段,因为它们会为每次读取引入运行时开销,当循环访问字段时,这些开销可能会变得痛苦,并且因为关键字表示错误的意图。(顺便说一句,据我所知,Akka框架应用了类似的策略,除了Spring之外,Akka在这个问题上放了一些线@PostConstructvolatilevolatile

但是,此保证仅用于在自举后检索 Bean。如果在非字段引导后更改非字段,或者在 Bean 引用初始化期间泄漏该字段,则此保证不再适用。volatile

查看此较旧的博客文章,其中更详细地描述了此功能。显然,这个功能没有被记录下来,因为即使是Spring的人也知道(但很长一段时间没有做任何事情)。


答案 2

我应该担心某些数据写入其他bean的可见性并将其标记为易失性吗?

我看不出你有什么理由不这样做。Spring框架在调用@PostConstruct时没有提供额外的线程安全保证,因此通常的可见性问题仍然可能发生。一种常见的方法是声明 final,但如果你想多次修改字段,它显然不适合。someData

它是否是第一次写入字段并不重要。根据Java内存模型,重新排序/可见性问题适用于这两种情况。唯一的例外是最终字段,它可以在第一次时安全地写入,但以后的赋值(例如通过反射)不能保证是可见的。

volatile但是,可以保证其他线程的必要可见性。它还可以防止部分构造的数据对象的意外暴露。由于重新排序问题,可能会在完成所有必要的对象创建操作(包括构造函数操作和默认值赋值)之前分配引用。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. 最后但并非最不重要的一点是:我们正在谈论的这个内部监视器是一个实现细节。由于无证件,它不能保证永远存在,并且可能会在没有另行通知的情况下进行更改。

附注:上述所有内容对于bean都是如此,它们是多线程访问的主题。对于原型范围的bean来说,情况并非如此,除非它们显式地暴露给多个线程,例如通过注入到单例范围的bean中。


推荐