问题描述
最近我读了a great tutorial of Java Memory Model。它说JVM只保证 如果未使用同步,则 final 字段的可见性。然后我想到,当我们使用某些IoC框架时,通常会使用不受 final 语义保护的setter注入/字段注入。例如,
class SomeController {
private SomeService service;
@Inject
public void setService(SomeService s){
this.service = s;
}
}
是否在注入后某些线程可能读取过时的值service
?还是应该将service
标记为 volatile 字段?
解决方法
首先,您正在阅读的“教程”(对于这样一个复杂的主题来说是一个很奇怪的名称),它真的很古老了。此外,该文档还针对(通常)编写编译器或围绕JVM本身工作的人员。我仍然觉得这是一篇很棒的文章。
您肯定在特殊条件下保证了可见性;但是final
只是其中之一。至少有3个(但不限于):
-
使用适当的锁定字段
-
使用静态初始化程序
-
使用
volatile
字段。
最后,这称为“安全发布”,这全都涉及调用者在引用SomeController
实例的情况下如何感知其字段(service
)。是否可以保证看到非空的service
?
Spring保证它将是一个完全初始化的实例,但并非您可能认为的那样。 JLS
中有一个称为“ happens-before”的原则。它也称为“关系”之前的事件,因为它涉及两个方面。例如,一个执行写操作(调用setService
),一个执行读操作(使用service
)。据说当双方都遵循某些规则时,关系就得到了保证和满足(阅读部分看到一个非空service
)。这些规则非常严格地写成in the JLS。用更简单的话来说:当遵循其中一个规则时,可以保证只看到非null service
。您提到其中之一:
对易失性字段的写操作发生在每次对该字段的后续读取之前。
但是请注意,它并不是那里唯一的一个。
因此,例如,如果Spring在线程中进行所有注入,并且仅在上下文中调用Thread::start
的之后,那么就有一条规则in the JLS here
在启动线程中的任何操作之前,都会在线程上进行对start()的调用。
这将确保注入service
并正确地将其视为非null。
这里可能需要更多说明,所以这里是一个示例:
// (1) init Spring context and do the needed injections
// (2) call Thread::start with this context
// (3) use context in a different thread now
该JLS文档中需要遵循以下三个规则:
如果x和y是同一线程的动作,并且x按程序顺序位于y之前,则hb(x,y)。
这意味着(1)发生在(2)之前
在启动线程中的任何操作之前,都会在线程上进行对start()的调用。
这意味着(2)发生在(3)之前。
如果hb(x,y)和hb(y,z),则hb(x,z)。
这意味着(1)发生在-(3)之前。这是我们关心的问题,这只是Spring获得适当可见性的一种方法。