简体   繁体   English

Spring单例bean,地图和多线程

[英]Spring singleton beans, maps and multithreading

Sometimes I used to do things like this: 有时候我常常这样做:

@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
}

But, strictly speaking, it's buggy code, isn't it? 但是,严格来说,它是错误的代码,不是吗?

1) this.processors is not final, nor is its creation synchronized on the same monitor as every access to it. 1) this.processors不是最终的,它的创建也不会在每次访问时在同一个监视器上同步。 Thus, every thread - and this singleton can be called from arbitrary thread processing user request - may be observing its own value of this.processors which might be null. 因此,每个线程 - 以及这个单例都可以从任意线程处理用户请求中调用 - 可能正在观察它自己的this.processors值,它可能为null。

2) Even though no writes happen after the Map is initially populated, Javadoc offers no guarantees on which implementation will be used for the Map , so it might be an implementation not ensuring thread safety when the Map structure changes, or even if anything is modified, or at all. 2)即使在最初填充Map之后没有写入,Javadoc也不能保证将哪个实现用于Map ,因此它可能是一个实现,不能确保Map结构更改时的线程安全性,或者即使有任何修改或者根本没有。 And initial population is changes, so it may break thread safety for who knows how long. 初始人口变化,所以它可能会破坏谁知道多长时间的线程安全。 Collectors even offer the specialized toConcurrentMap() method, to address that problem - so, at a bare minimum, I should have been using it instead. Collectors甚至提供专门的toConcurrentMap()方法来解决这个问题 - 所以,至少我应该使用它。

But even if I use toConcurrentMap() in #2, I will not be able to make my field final , because then I'll not be able to initialize it in a setter. 但即使我在#2中使用toConcurrentMap() ,我也无法将我的字段设为final ,因为那时我将无法在setter中初始化它。 So here are my choices: 所以这是我的选择:

a) Initialize and populate the Map in an autowired constructor, which frankly I prefer. a)在自动装配的构造函数中初始化并填充Map ,坦率地说我更喜欢。 But so few teams do that, so what if we abstain from that solution? 但是很少有团队这样做,那么如果我们放弃那个解决方案呢? What other choices exist? 还有哪些其他选择?

b) Initialize the Map to an empty final ConcurrentHashMap , then populate it in a setter. b)将Map初始化为空的final ConcurrentHashMap ,然后在setter中填充它。 This is possible, but we'll have to list.forEach() then map.put() . 这是可能的,但我们必须list.forEach()然后map.put() This looks like it's still Java 6; 看起来它仍然是Java 6; or we could definitely do map.addAll(list....toMap()) but its useless duplication of the Map , even if temporary. 或者我们肯定可以做map.addAll(list....toMap())但它无用的重复Map ,即使是临时的。

c) Use volatile on the field. c)在场上使用volatile Slightly degrades performance without any need, because after some point the field never gets changed. 在没有任何需要的情况下稍微降低性能,因为在某些时候,该字段永远不会改变。

d) Use synchronized to access the field and read its values. d)使用synchronized来访问字段并读取其值。 Clearly even worse than (c). 显然甚至比(c)更糟糕。

Also, any of those methods will make the reader think that the code actually wants some multithreading reads/writes to the Map , while actually, it's just multithreaded reading. 此外,任何这些方法都会让读者认为代码实际上需要对Map多线程读/写,而实际上,它只是多线程读取。

So, what does a reasonable guru do when they want something like that? 那么,当一个合理的大师想要这样的东西时会做些什么呢?

At this point, the best solution seems to be the one with a volatile field, assigned in a setter by using toConcurrentMap . 此时,最佳解决方案似乎是具有volatile字段的解决方案,通过使用toConcurrentMap在setter中分配。 Is there anything better? 有更好的吗? Or maybe I am just making up problems no one ever actually encountered? 或者也许我只是在解决实际遇到的问题?

Or maybe I am just making up problems no one ever actually encountered? 或者也许我只是在解决实际遇到的问题?

I think may be conflating your assignment with the problems historically seen from double-checked locking: 我认为可能会将您的任务与历史上从双重检查锁定中看到的问题混为一谈:

private Foo foo;  // this is an instance variable

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

This code appears to be thread-safe: you do an initial, presumed quick, check to verify that the value has not been initialized yet, and if it hasn't you initialize in a synchronized block. 此代码似乎是线程安全的:您执行初始化,假设快速检查以验证该值尚未初始化,以及是否尚未在同步块中初始化。 The problem is that the new operation is distinct from the constructor call, and some implementations were assigning the reference returned by new to the variable before the constructor ran. 问题是new操作与构造函数调用不同,并且一些实现在构造函数运行之前将new返回的引用赋值给变量。 The result was that another thread could see that value before the constructor completed. 结果是另一个线程可以在构造函数完成之前看到该值。

In your case, however, you are assigning the variable based on the result of a function call. 但是,在您的情况下,您将根据函数调用的结果分配变量。 The Map created by the function call is not assigned to the variable until the function returns. 在函数返回之前,函数调用创建的Map不会分配给变量。 The compiler (including Hotspot) is not permitted to re-order this operation because such a change would be visible to the thread that's executing the function , and would not therefore be sequentially consistent per JLS 17.4.3 . 不允许编译器(包括Hotspot)重新排序此操作,因为这样的更改对于正在执行该函数的线程是可见 ,因此不会按照JLS 17.4.3顺序一致。

That out of the way, here are some additional comments: 除此之外,这里有一些额外的评论:

Initialize and populate the Map in an autowired constructor, which frankly I prefer 在自动装配的构造函数中初始化并填充Map,坦率地说我更喜欢

As do the creators of the Guice dependency injection framework. 和Guice依赖注入框架的创建者一样。 One reason to prefer constructor injection is that you know that you'll never see the bean in an inconsistent state. 更喜欢构造函数注入的一个原因是你知道你永远不会看到bean处于不一致状态。

The reason that Spring encourages (or at least does not discourage) setter injection is because it makes circular references possible. Spring鼓励(或至少不劝阻)二传手注射的原因是因为它使循环引用成为可能。 You can decide for yourself whether circular references are a good idea. 您可以自己决定循环引用是否是一个好主意。

Initialize the Map to an empty final ConcurrentHashMap, then populate it in a setter 将Map初始化为空的最终ConcurrentHashMap,然后在setter中填充它

This is a bad idea because it's likely that other threads will see a partially constructed map. 这是个坏主意,因为其他线程可能会看到部分构造的地图。 It's far better to see either null or a fully-constructed map, because you can compensate for the first case. 看到null或完全构造的地图要好得多,因为你可以补偿第一种情况。

Use volatile on the field. 在场上使用volatile。 Slightly degrades performance without any need 在没有任何需要的情况下轻微降低性能

Use synchronized to access the field and read its values. 使用synchronized可访问该字段并读取其值。 Clearly even worse than (c). 显然甚至比(c)更糟糕。

Don't let perceived performance impacts keep you from writing correct code. 不要让感知到的性能影响阻止您编写正确的代码。 The only time that synchronization will significantly impact your performance is when concurrent threads access a synchronized variable/method within a tight loop. 同步将显着影响性能的唯一时间是并发线程在紧密循环内访问同步变量/方法。 If you're not in a loop, then the memory barrier adds an irrelevant amount of time to your call (and even in a loop it's minimal unless you need to wait for a value to arrive in your core's cache). 如果你不在循环中,那么内存屏障会为你的调用增加一个无关的时间(即使在循环中它也是最小的,除非你需要等待一个值到达核心的缓存)。

In this case it doesn't matter, but I would guess that getProcessors() takes a tiny percentage of your total execution time, and that a far larger amount of time is taken by running the processor(s). 在这种情况下它并不重要,但我猜想getProcessors()占用总执行时间的一小部分,并且运行处理器需要花费更多的时间。

Thanks to hints of commenters here, after googling a bit I was able to find not a reference to Spring manual but at least a matter-of-fact guarantee, see Should I mark object attributes as volatile if I init them in @PostConstruct in Spring Framework? 感谢这里的一些评论者,在谷歌搜索之后,我找不到对Spring手册的引用,但至少是事实上的保证,请参阅我是否应该在Spring中的@PostConstruct中将对象属性标记为volatile框架? - the updated part of accepted answer. - 接受答案的更新部分。 In essence it says that every lookup of a particular bean in a context, and hence, roughly, every injection of that bean, is preceded by locking on some monitor, and bean initialization also happens while locked on that same monitor, establishing happens-before relationship between bean initialization and bean injection. 实质上它表示在上下文中每次查找特定bean,因此,粗略地说,每次注入该bean之前都会锁定某个监视器,并且在锁定在同一监视器上时也会发生bean初始化,建立之前发生bean初始化和bean注入之间的关系。 More specifially, everything done during bean initialization (like, assignment of processors in MyBean initialization) happens-before subsequent injections of that bean - and the bean is only used after it has been injected. 更具体地说,在bean初始化期间完成的所有事情(比如,MyBean初始化中的处理器的分配)发生 - 在随后注入该bean之前 - 并且bean仅在注入之后使用。 So the author says no volatile is necessary, unless we are going to change the field after that. 所以作者说不需要挥发性 ,除非我们在那之后改变领域。

That would be my accepted answer (in combination with toConcurrentMap) if not for 2 "buts". 如果不是2“buts”那将是我接受的答案(与toConcurrentMap结合使用)。

1) This does not mean injection of non-initialized beans offers that same happens-before. 1)这并不意味着注入非初始化的bean提供了相同的事情。 And injection of non-initialized beans happens more often that some think. 一些人认为,非初始化豆的注入更常发生。 In case of circular dependencied, which are better kept rare but look valid sometimes. 在循环依赖的情况下,哪些更好地保持罕见但有时看起来有效。 In case of lazy initialized beans. 在惰性初始化bean的情况下。 Some libraries (AFAIK even some of Spring projects) introduce circular dependencies, I saw that myself. 一些库(AFAIK甚至一些Spring项目)引入了循环依赖,我自己也看到了。 And sometimes you introduce circular deps by accident, that is not treated as error by default. 有时您偶然会引入循环deps,默认情况下不会将其视为错误。 Of course, reasonable code does not use non-initialized beans, but since MyBean could be injected to some bean X before it's initialized, there will be no happens-before after it's initalized, annihilating our guarantees. 当然,合理的代码不使用未初始化豆,但由于它的初始化之前为myBean可以注入一些豆X,就不会有之前发生它initalized ,消灭我们的保证。

2) This is not even a documented feature. 2)这甚至不是一个记录的功能。 Still! 仍然! But recently it has at least been put on a backlog. 但最近它至少已经积压了。 See https://github.com/spring-projects/spring-framework/issues/8986 - still, until they documented it we cannot assume it's not subject to changes. 请参阅https://github.com/spring-projects/spring-framework/issues/8986 - 仍然,直到他们记录下来,我们不能认为它不会发生变化。 Bah, even when they do, it still may be changed in some next version, but at least that will be reflected in some changelist or whatnot. Bah,即使他们这样做,它仍然可能在下一个版本中被更改,但至少这将反映在一些更改列表或诸如此类的东西中。

So, taking those 2 notes into consideration, especially the 1st, I am inclined to say that volatile+toConcurrentMap is the way to go. 因此,考虑到这两个注释,特别是第一个,我倾向于说volatile + toConcurrentMap是要走的路。 Right? 对?

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM