Java IoC框架如何确保线程安全?

huangapple go评论76阅读模式
英文:

How does java IoC framework ensure thread safety?

问题

最近我阅读了一篇关于Java内存模型的很棒的教程,地址在:http://www.cs.umd.edu/~pugh/java/memoryModel/jsr-133-faq.html。教程中提到,JVM只在没有使用同步的情况下保证final字段的可见性。这让我想到了,当我们使用一些IoC框架时,通常会使用setter注入/字段注入,这些方式并没有受到final语义的保护。例如,

class SomeController {
    private SomeService service;

    @Inject
    public void setService(SomeService s){
        this.service = s;
    }
}

如果某个线程在注入后读取了service的旧值,这种情况是否可能发生?或者我们应该将service标记为volatile字段?

英文:

Recently I have read a great tutorial of Java Memory Model. It says JVM only guarantees
the visibility of final field if no synchronization used. Then it occurs to me that when we use some IoC frameworks, we usually use setter injection/field injection which are not protected by final semantics. For example,

class SomeController {
    private SomeService service;

    @Inject
    public void setService(SomeService s){
        this.service = s;
    }
}

If it is possible for some thread to read a stale value of service after the injection? Or should we mark service as a volatile field?

答案1

得分: 4

首先,您正在阅读一个非常古老的“教程”(对于如此复杂的主题来说,这个名字确实有点奇怪)。此外,该文档的目标受众通常是编写编译器或直接与JVM本身相关的人;我仍然认为这是一篇出色的文章。

您说得对,可见性在特殊条件下是有保证的;但final只是其中之一。至少有3种方法可以实现可见性(而不限于此):

  • 使用适当的锁定字段
  • 使用静态初始化器
  • 使用volatile字段

最终,这被称为“安全发布”,它关乎调用者如何看待SomeController实例的字段(service)。他们是否有保证看到一个非空的service

Spring保证它将是一个完全初始化的实例,但不是您可能认为的那种方式。在JLS中有一个称为“happens-before”的原则。它也被称为“happens-before关系”,因为它涉及两个参与方,例如执行写操作(调用setService)的参与方和执行读操作(使用service)的参与方。有一些规则,严格地写在JLS中。简单来说:只有在遵循这些规则之一时,才保证看到一个非空的service。其中一个由您提到:

对volatile字段的写入发生在对该字段的每次后续读取之前。

但请注意,这并不是唯一的规则。

因此,如果Spring,例如,将所有注入操作都在一个线程中执行,并且仅在之后在其上下文上调用Thread::start,那么JLS中的规则在这里将保证service被注入并正确地视为非空。

这可能需要进一步解释,所以这里有一个示例:

// (1) 初始化Spring上下文并进行所需的注入
// (2) 使用此上下文调用Thread::start
// (3) 现在在不同的线程中使用上下文

在此示例中,我们需要遵循来自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实现正确可见性的一种方式。

英文:

First of all you are reading a "tutorial" (that is a rather weird name for such a complicated topic) that is really old. Also, that document is targeted towards people that (usually) write compilers or work around the JVM itself; I still find it an excellent write-up.

You are correct that visibility is guaranteed under special conditions; but final is only one of them. There are at least 3 (and not limited to):

  • Use a proper locked field

  • Use static initializers

  • Use a volatile field.

In the end, this is called "safe publishing" and it is all about how callers, given a reference to an instance of SomeController, will perceive its fields (service). Are they guaranteed to see a non-null service?

Spring guarantees that it will be a fully initialized instance, but not in the sense that you might think. There is a principle in the JLS called "happens-before". It is also called a happens-before "relationship", since it involves two parties. For example one that does a write (calls that setService) and one that does a read (uses that service). It is said that the relationship is guaranteed and fulfilled (reading part sees a non-null service) when both parties follow some rules. Those rules are very strictly written in the JLS. In simpler words: you are guaranteed to see a non-null service only when one of those rules are followed. One of them is mentioned by you:

> A write to a volatile field happens-before every subsequent read of that field.

But notice that it is not the only one there.

So, if Spring, for example, does all the injections in a Thread, and only after that calls Thread::start on it's context, then there is a rule in the JLS here

> A call to start() on a thread happens-before any actions in the started thread.

that will guarantee that service is injected and correctly seen as non-null.


This probably needs a bit more explanation here, so here is an example:

// (1) init Spring context and do the needed injections

// (2) call Thread::start with this context

// (3) use context in a different thread now

There are three rules that we need to follow here from that JLS document:

> If x and y are actions of the same thread and x comes before y in program order, then hb(x, y).

This means that (1) happens-before (2)

> A call to start() on a thread happens-before any actions in the started thread.

This means (2) happens-before (3).

> If hb(x, y) and hb(y, z), then hb(x, z).

This means (1) happens-before (3). And this is the one we care about and it's just one way Spring can achieve proper visibility.

huangapple
  • 本文由 发表于 2020年9月13日 17:31:50
  • 转载请务必保留本文链接:https://go.coder-hub.com/63869205.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定