Spring单例bean,映射和多线程

时间:2019-03-27 06:57:01

标签: java spring multithreading

有时候我曾经做过这样的事情:

@Component class MyBean {
  private Map<TypeKey, Processor> processors;

  @Autowired void setProcessors(List<Processor> processors) {
    this.processors = processors.stream().....toMap(Processor::getTypeKey, x -> x);
  }

  //some more methods reading this.processors
}

但是,严格来说,这是错误的代码,不是吗?

1)this.processors不是最终的,它的创建也不是每次访问都在同一监视器上同步的。因此,每个线程(可以从任意线程处理用户请求中调用此单例)可能正在观察自己的值this.processors,该值可能为空。

2)即使在最初填充Map之后没有写操作,Javadoc也不能保证Map将使用哪种实现,因此它可能是一种无法确保线程安全的实现。 Map结构会发生变化,或者即使进行了任何修改,也可能根本没有变化。而且初始填充会发生变化,因此对于知道多长时间的人来说,它可能会破坏线程安全性。 Collectors甚至提供了专门的toConcurrentMap()方法来解决该问题-因此,至少我应该一直在使用它。

但是,即使我在#2中使用了toConcurrentMap(),也无法将我的字段设为final,因为那样的话,我将无法在设置器中对其进行初始化。所以这是我的选择:

a)在自动装配的构造函数中初始化Map并填充它,坦率地说,我更喜欢。但是很少有团队这样做,如果我们放弃该解决方案该怎么办?还有其他选择吗?

b)将Map初始化为空的final ConcurrentHashMap,然后将其填充到设置器中。这是可能的,但是我们必须先list.forEach()然后map.put()。看起来还是Java 6。否则我们肯定可以map.addAll(list....toMap()),但是Map的重复是无用的,即使是暂时的。

c)在字段上使用volatile。不必要地会稍微降低性能,因为在某些时候,该字段永远不会改变。

d)使用synchronized访问该字段并读取其值。显然甚至比(c)还差。

此外,这些方法中的任何一种都会使读者认为代码实际上希望对Map进行多线程读取/写入,而实际上,它只是多线程读取。

那么,当一个理性的大师想要这样的东西时,他们会做什么呢?

在这一点上,最好的解决方案似乎是使用volatile在设置器中分配一个toConcurrentMap字段的解决方案。有更好的吗?也许我只是在弥补没人真正遇到过的问题?

2 个答案:

答案 0 :(得分:0)

  

或者也许我只是在解决没有人真正遇到过的问题?

我认为这可能会使您的分配与历史上从双重检查锁定中看到的问题混淆:

private Foo foo;  // this is an instance variable

public Foo getFoo() {
    if (foo != null) {
        synchronized (this) {
            if (foo != null) {
                foo = new Foo();
            }
        }
    }
    return foo;
}

此代码似乎是是线程安全的:您进行了初始的,假定的快速操作,请检查以确认该值尚未初始化,如果尚未初始化,请在同步状态下进行初始化块。问题在于new操作与构造函数调用不同,并且某些实现在构造函数运行之前将new返回的引用分配给变量。结果是另一个线程可以在构造函数完成之前看到该值。

但是,根据您的情况,您是根据函数调用的结果分配变量。在函数返回之前,函数调用创建的Map不会分配给变量。不允许编译器(包括Hotspot)重新排序此操作,因为这样的更改对于执行函数的线程是可见的,因此每个JLS 17.4.3都不是顺序一致的。

顺便说一下,这里还有一些其他评论:

  

在自动装配的构造函数中初始化并填充Map,坦率地说,我更喜欢

Guice依赖项注入框架的创建者也是如此。偏爱构造函数注入的一个原因是,您知道您永远不会看到Bean处于不一致状态。

Spring鼓励(或至少不阻止)setter注入的原因是因为它使循环引用成为可能。您可以自己决定是否使用循环引用。

  

将地图初始化为空的最终ConcurrentHashMap,然后将其填充到设置器中

这是一个不好的主意,因为其他线程可能会看到部分构造的映射,这很可能是。最好查看null或完整的地图,因为您可以补偿第一种情况。

  

在字段上使用volatile。不必要地稍微降低性能

     

使用同步访问该字段并读取其值。显然甚至比(c)还差。

不要让感知的性能影响使您无法编写正确的代码。同步将严重影响性能的唯一时间是并发线程在紧密循环中访问同步变量/方法时。如果您不在循环中,那么内存屏障会为您的调用添加无关紧要的时间(即使在循环中,它也是最少的,除非您需要等待一个值到达内核的缓存中)。

在这种情况下,这并不重要,但是我猜想getProcessors()只占您总执行时间的一小部分,而运行处理器所花费的时间要大得多。

答案 1 :(得分:0)

由于这里有评论者的提示,在进行了一段时间的搜索之后,我找不到对Spring手册的引用,但至少是事后保证,请参阅Should I mark object attributes as volatile if I init them in @PostConstruct in Spring Framework?-更新了 已接受答案的一部分。从本质上讲,它表示在上下文中对特定bean的每次查找(因此,大概是对该bean的每次注入)都在锁定某个监视器之前进行,并且bean初始化也在锁定在同一监视器上时发生,建立之前bean初始化和bean注入之间的关系。更具体地说,在bean初始化期间完成的所有操作(例如,在MyBean初始化中分配处理器)发生在该bean的后续注入之前-并且该bean仅在其已注入之后使用被注射。因此,作者说不需要 volatile 是必要的,除非我们在那之后要更改该字段。

如果不是2个“ buts”,那将是我接受的答案(与toConcurrentMap结合使用)。

1)这并不意味着注入未初始化的bean会提供与之前相同的操作。一些人认为,注入未初始化的bean的频率更高。在循环依赖的情况下,最好将它们保留得很少,但有时看起来有效。如果是懒惰的初始化bean。有些库(甚至是AFAIK甚至是一些Spring项目)都引入了循环依赖,我自己也看到了。有时,您会偶然引入圆形dep,默认情况下不会将其视为错误。当然,合理的代码不会使用未初始化的bean,但是由于MyBean可以在初始化之前的 中注入到某些bean X中,所以不会发生在之后的之前令人陶醉,消灭了我们的保证。

2)这甚至不是文档功能。仍然!但是最近至少已经积压了。请参见https://github.com/spring-projects/spring-framework/issues/8986-在他们记录下来之前,我们不能假定它不受更改。 ah,即使他们这样做了,它仍可能会在下一版本中进行更改,但至少会在某些更改列表中反映出来。

因此,考虑到这两个音符,尤其是第一个音符,我倾向于说 volatile + toConcurrentMap 是可行的方法。对吧?