【问题标题】:Are injected (@Inject) fields safely published?注入 (@Inject) 字段是否安全发布?
【发布时间】:2012-12-01 14:44:27
【问题描述】:

当我在类中使用字段注入时,如下所示:

@Inject
private MyClass myField;

我可以对此字段的“safe publication”状态做出任何假设吗?或者换一种说法,假设 MyClass 本身是线程安全的,那么在使用该字段时我应该注意哪些并发风险?

我的直觉通常是尽可能创建所有最终字段,但这不适用于字段注入。当然,我可以使用构造函数注入,但是我通常最终不得不创建一个额外的“假”无参数构造函数来进行代理。问题不大,但使用字段注入更方便。另一种选择可能是将字段标记为易失性(甚至使用锁定...),但这真的有必要吗?

JSR-299 规范似乎没有回答这个问题。我在 Weld 等实现上使用 CDI。

  • 我要注入的对象被多个线程使用(例如,它是@ApplicationScoped)。我想要这个。
  • 我了解,如果 MyClass 是不可变的,则无需担心安全发布。但我不一定只注入不可变对象。
  • MyClass 本身被假定为线程安全的;这不是我关心的。严格来说,关注的是不安全的发布,例如由于 Java 内存模型的规则,线程可能会看到 MyClass 的半构造实例。

【问题讨论】:

  • 是的,使用 CDI 有时需要在使用访问修饰符和 final 时编写更少防御性的代码。但毫无疑问,线程安全将是 CDI 规范中的一个疏忽。所以虽然我无法解释这一点,但我自己觉得很省钱。我们的一个应用程序也有 60.000 个用户,并且运行良好

标签: java thread-safety cdi weld


【解决方案1】:

我总是使用构造函数注入。那么您的字段可能是最终的,并且它们的线程安全性毫无疑问。

【讨论】:

  • 我也是,但更多是出于直觉而非扎实的知识。这种方法迫使您提供一个笨拙的“虚拟”无参数构造函数,它只是将所有字段都归零,对吗?否则该类将不可代理。
  • 你可以用@Inject注解一个构造函数,它会将所有的args注入到构造函数中,然后你可以像往常一样继续。当然,任何限定符都需要在构造函数参数上。
  • 我知道构造函数注入是如何工作的,如原始问题中所述。但是当我添加一个带参数的构造函数(注入注释与否)时,我还需要添加一个显式的无参数构造函数以保持类可代理。在最终字段的情况下,我需要将所有字段设置为某个值(在这种情况下为 null)。这有点尴尬。
  • 不能说我对 CDI / Weld 代理有足够的了解来帮助解决这个问题。也就是说,我通常只尝试代理接口(而不是具体类),然后就不需要虚拟构造函数了。
  • 关于使用接口的好处。但是,有时它们并没有用,只需添加样板即可。
【解决方案2】:

使用注入实例时的任何并发风险取决于该实例的有效范围。

如果MyClass 在默认的@Dependent 范围内,每个注入点都将获得自己的实例。您在线程安全方面采取的预防措施与您自己调用new MyClass() 相同。如果您从多个线程访问该实例,则需要确保 MyClass 是线程安全的或围绕它提供一些同步。

如果MyClass 处于更广泛的范围内,例如@SessionScoped@ApplicaionScoped,则可以将同一实例(或它的代理)注入同一上下文中的多个注入点。例如,如果您有来自同一个会话的并行浏览器请求访问MyClass 并且MyClass 被注释为@SessionScoped,您可以有多个线程并行访问同一个实例。 CDI 不会为您提供任何同步,因此您必须确保 MyClass 是线程安全的。

【讨论】:

  • 我的问题不在于注入的实例是否可以被多个线程访问 - 我知道,并且确实想要它。一个典型的用例是一个 JAX-RS 资源类,它是 ApplicationScoped 并注入了一些字段。当然,这意味着许多线程将访问同一个实例。 MyClass 本身可能是完全线程安全的(例如,所有字段访问都由锁保护)并且仍然可以以不安全的方式发布(参见this)。 CDI 对此有什么帮助吗?
【解决方案3】:

也许规范中故意忽略了这种情况下的线程安全,这意味着线程安全没有得到保证。

让我们想一想:如果一个线程写入的字段被其他线程读取,除非存在某种形式的发生前关系,否则其他线程可能会读取陈旧数据。 Guice 最终使用反射来设置 myField 的值,或者它可以使用自动生成的设置器。没有happens-before关系,因此反射写入发生在字段读取之前或方法调用发生在字段读取之前(除非使用锁、易失性或其他方式形成了happens-before关系)。

因此,我会说看到空值的可能性(可能相当低)。

编辑:根据http://bit.ly/1m4AUIz 在构造函数结束后写入最终字段(通过反射)与在构造函数中初始化字段具有相同的语义。因此,将 Guice 注入的字段设置为最终字段,将它们设置为 null 并且它应该可以正常工作。这确实是一个非常黑暗的 JVM 角落 :-) 此外,根据http://bit.ly/1m4AwJU Guice 只在一个线程中执行注入,这使其成为线程安全的......在性能方面对我来说似乎很奇怪,但显然它是这样工作的。

【讨论】:

  • 很高兴知道,但 Guice 不是 CDI 实现,正如 Rob 的回答中所解释的,CDI 排除了 final 字段的注入。
【解决方案4】:

我相信您可以基于 java 内存模型的第 9.1.1 节:http://www.cs.umd.edu/~pugh/java/memoryModel/jsr133.pdf 9.1.1 最终字段的构造后修改 ... 最终字段的冻结发生设置 final 字段的构造函数的末尾,每次通过反射修改 final 字段之后其他特殊机制。 ...

一些相关的 Guice 讨论是: http://markmail.org/message/fxs5k32dihpoy5ry#query:bob%20lee%20constructor%20injection+page:1+mid:fxs5k32dihpoy5ry+state:results

.. 和http://www.theserverside.com/discussions/thread.tss?thread_id=52252#284713

如果 DI 框架做出这样的明确声明,那就太好了。

【讨论】:

  • 虽然 Guice 似乎允许在 final 字段上使用字段注入,但 CDI 规范明确排除:“注入的字段是 bean 类的非静态、非 final 字段...... ”(3.8)。此外,CDI 规范似乎没有说明注入是否在单个线程中完成。
猜你喜欢
  • 2016-04-02
  • 1970-01-01
  • 1970-01-01
  • 2021-12-31
  • 1970-01-01
  • 1970-01-01
  • 2012-07-24
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多