【问题标题】:CDI PostConstruct and volatile fieldsCDI PostConstruct 和 volatile 字段
【发布时间】:2019-04-13 03:11:31
【问题描述】:

当我们想要有条件地初始化 bean 的某些字段时使用后构造方法,我们是否需要关心字段的波动性,因为它是一个多线程环境?

说,我们有这样的事情:

@ApplicationScoped
public class FooService {

    private final ConfigurationService configurationService;

    private FooBean fooBean;

    @Inject
    FooService(ConfigurationService configurationService) {
         this.configurationService = configurationService;
    }

    void init(@Observes @Initialized(ApplicationScoped.class) Object ignored) {
        if (configurationService.isFooBeanInitialisationEnabled()) {
             fooBean = initialiseFooBean(configurationService); // some initialisation
        }
    }

    void cleanup(@Observes @Destroyed(ApplicationScoped.class) Object ignored) {
       if (fooBean != null) {
           fooBean.cleanup();
       }
    }
}

那么应该将fooBean 包装到,比如说AtomicReference 或者是volatile,或者这将是一个多余的额外保护?

P.S. 在这种特殊情况下,它可以重新表述为:构建后事件和销毁后事件是否由同一个线程执行?但是,我想为更一般的情况提供答案。

【问题讨论】:

    标签: java jakarta-ee concurrency cdi volatile


    【解决方案1】:

    我会说这取决于哪个线程实际启动和销毁上下文。 如果您使用常规事件,它们是同步的(异步事件已在 CDI 2.0 中添加ObservesAsync,请参阅 Java EE 8: Sending asynchronous CDI 2.0 events with ManagedExecutorService ) 所以它们在与调用者相同的线程中被调用。

    一般来说,我不认为使用相同的线程(在应用程序服务器或独立应用程序中),所以我建议使用volatile 来确保看到正确的值(基本上是在销毁线程上看到的构造值)。但是,以并发方式启动和销毁您的应用程序并不是一个经常发生的用例......

    【讨论】:

      【解决方案2】:

      FooService 是一个单例,在应用程序中的所有托管 bean 之间共享。

      Annotation Type ApplicationScoped

      private FooBean fooBean 是单例对象的状态。

      默认情况下,CDI 不管理并发,因此它是开发人员的责任。

      在这种特殊情况下,它可以重新表述为:构造后事件和销毁后事件是否由同一线程执行?

      CDI specification 不限制容器使用相同的线程来初始化和销毁​​应用程序上下文。此行为是特定于实现的。在一般情况下,这些线程会有所不同,因为初始化发生在处理对应用程序的第一个请求的线程上,而销毁发生在来自管理控制台的线程处理请求上。

      【讨论】:

      • 规范中关于线程管理的部分是否有所改变?
      • 这个post by Stephan Knitelius总结了CDI规范中并发管理的情况,并提供了如何使用CDI拦截器复制EJB并发机制的指导。
      【解决方案3】:

      您可以将并发管理委托给 EJB 容器 - 如果您的运行时环境包括一个。

      在这种情况下,volatileAtomicReference 都不需要!

      以下定义将完成这项工作:

      @javax.ejb.Startup   // initialize on application start
      @javax.ejb.Singleton // EJB Singleton
      public class FooService {
      
          private final ConfigurationService configurationService;
      
          private FooBean fooBean;
      
          @javax.inject.Inject
          FooService(ConfigurationService configurationService) {
               this.configurationService = configurationService;
          }
      
      
          @javax.annotation.PostConstruct
          void init() {
              if (configurationService.isFooBeanInitialisationEnabled()) {
                   fooBean = initialiseFooBean(configurationService); // some initialisation
              }
          }
      
          @javax.annotation.PreDestroy
          void cleanup() {
             if (fooBean != null) {
                 fooBean.cleanup();
             }
          }
      }
      

      【讨论】:

        【解决方案4】:

        According to the specification:

        在初始化应用程序上下文时,同步触发带有限定符 @Initialized(ApplicationScoped.class) 的事件。

        当应用程序上下文即将被销毁时,即在实际销毁之前,同步触发带有限定符 @BeforeDestroyed(ApplicationScoped.class) 的事件。

        一个带有限定符@Destroyed(ApplicationScoped.class) 的事件在应用上下文被销毁时同步触发,即在实际销毁之后。

        根据这个演示Bean manager lifecycle:bean 管理器的生命周期在进程的不同状态之间是同步的,并且保持顺序:“在 init 之前不销毁”。

        Jboss 是 CDI 2.0 的规范负责人

        我没有看到任何需要易失性/保护的情况。即使 T1 初始化然后 T2 销毁,它也将是 T1 then T2,而不是同时 T1 和 T2。

        即使它是同时发生的,如果出现问题,它也意味着奇怪的场景,CDI 运行时之外的边缘场景:

        • T2 调用 destroyfooBean 为空,现在“缓存”在寄存器中)
        • 然后T1调用init:destroy before init,此时我们在CDI的第4维),
        • 然后 T2 调用 destroyfooBean 已缓存在寄存器中,因此值为 null)。

        或者

        • T2 调用访问fooBean 的方法(fooBean 为空,现在“缓存”在寄存器中)
        • 然后T1调用init:T1被初始化,而fooBean已经被T2使用,此时我们处于CDI的第四维
        • 然后 T2 调用 destroyfooBean 已缓存在寄存器中,因此值为 null)。

        【讨论】:

        • 感谢您的规范!然而,对于缓存的值,它不需要被访问以放入 L* CPU 缓存中。它甚至不需要是并发调用。 volatile 保证 CPU 缓存确实包含任何线程写入的最新值。如果initdestroy 之间存在并发,您可能会得到null 值。
        • it could happen that you get null value 如何以及何时? x86 上的缓存是连贯且易失的,主要是关于不缓存到寄存器而不是 CPU 缓存
        • 首先,假设initdestroy同时执行,destroy首先到达fooBean字段,它会看到null有或没有volatilevolatile 可能提供帮助的另一种情况是,如果一个线程执行 initfooBean 缓存在所有 L1 缓存上,则它仅更改其核心中的值,而其他核心仍将 null 保存在其 L1 缓存中.如果destroy 在没有对缓存进行其他操作的情况下执行,它可以看到null,而不会违反Java 规范。
        • init and destroy execute at the same time:我们同意这种情况是不可能的吗? is cached on all L1 caches, it changes only value in the value in its core while the other cores still hold null in their L1 caches.:缓存一致性机制将保证在其他一级缓存中,条目无效。所以如果destroy之后执行,它就不会看到null
        猜你喜欢
        • 2011-06-26
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2013-05-23
        • 2015-06-01
        • 2014-11-07
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多