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

Pio*_*ler 18 java concurrency spring volatile

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

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

  public SomeService() { }

  @PostConstruct
  public void init() {
     someData = new Data(....);
  }
}
Run Code Online (Sandbox Code Playgroud)

我应该担心someData其他豆类的可见性并标记它volatile吗?

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

第二种情况:如果我覆盖了@PostConstruct(例如在构造函数中进行显式初始化或初始化之后),那么写入@PostConstruct 将不会首先写入此属性?

Raf*_*ter 23

Spring框架没有绑定到Java编程语言,它只是一个框架.因此,在一般情况下,你需要标记是由不同的线程访问的非最终字段volatile.在一天结束时,Spring bean只不过是一个Java对象,所有语言规则都适用.

final字段在Java编程语言中得到特殊处理.亚历山大Shipilev,在Oracle的性能的家伙,写了一大篇关于此事.简而言之,当构造函数初始化final字段时,用于设置字段值的程序集会添加一个额外的内存屏障,以确保任何线程都能正确地看到该字段.

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

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

简而言之,是的.如果某个字段可以被不同的线程访问,那么就可以.当我只考虑问题几秒钟(感谢Jk1进行修正)时,不要犯同样的错误,并根据Java代码的执行顺序进行思考.您可能认为您的Spring应用程序上下文是在单个线程中引导的.这意味着bootstraping线程不会出现非易失性字段的问题.因此,只要您没有将应用程序上下文暴露给另一个线程,直到它完全初始化,即调用带注释的方法,您可能会认为所有内容都是有序的.像这样思考,您可以假设,只要您在此引导程序之后不更改字段,其他线程就没有机会缓存错误的字段值.

相反,允许编译的代码重新排序指令,即使@PostConstruct在相关bean暴露给Java代码中的另一个线程之前调用了-annotated方法,这种情况发生之前 -关系不一定保留在编译代码中运行.因此,另一个线程可能总是读取并缓存非volatile字段,而它尚未初始化或甚至部分初始化.这可能会引入微妙的错误,但遗憾的是,Spring文档并没有提到这个警告.JMM的这些细节是我个人更喜欢final字段和构造函数注入的原因.

更新:根据另一个问题的答案,有些情况下不标记该字段volatile仍然会产生有效结果.我进一步调查了这一点,Spring框架实际上保证了一定数量的安全- 在开箱即用之前.看看JLS关于发生在之前的关系,它明确指出:

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

Spring框架使用了这个.所有bean都存储在一个映射中,每次从这个映射注册或检索bean时,Spring都会获取一个特定的监视器.结果,在注册完全初始化的bean之后,同一个监视器被解锁,并且在从另一个线程检索相同的bean之前它被锁定.这迫使另一个线程遵守由Java代码的执行顺序反映的before-before关系.因此,如果你引导bean一次,那么访问完全初始化bean的所有线程都将看到这种状态,只要它们以规范方式访问bean(即通过查询应用程序上下文或自动编程进行显式检索).这使得例如setter注入或@PostConstruct方法的使用即使没有声明字段也是如此volatile.事实上,你应该避免使用volatile字段,因为它们会为每次读取引入运行时开销,这会在访问循环中的字段时产生痛苦,并且因为关键字表示错误的意图.(顺便说一下,据我所知,Akka框架应用了一个类似的策略,除了Spring之外,Akka 在这个问题上放下了一些线索.)

但是,这种保证仅用于在引导程序之后检索bean.如果volatile在引导程序之后更改非字段,或者在初始化期间泄漏bean引用,则此保证不再适用.

查看此较旧的博客条目,其中详细介绍了此功能.显然,这个功能没有记录,因为即使Spring人都知道(但很长一段时间没有做任何事情).


Jk1*_*Jk1 6

我是否应该担心someData写入其他bean的可见性并将其标记为volatile?

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

如果它是第一次写入该字段应该没关系.根据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);
    }
}
Run Code Online (Sandbox Code Playgroud)

可能会为您提供线程安全属性@PostConstruct,但出于多种原因,我不认为这是足够的保证:

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

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

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

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

  • 还有一点我想提一下:即使真正的实现包含提供可见性保证的代码,比如`synchronized`块,除非javadoc和/或其他文档明确指出可见性保证是合同的一部分,否则你不应该依赖实施,因为它可以在不宣布战争的情况下进行更改. (2认同)