【发布时间】:2019-03-27 06:57:01
【问题描述】:
有时我会这样做:
@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 时,它可能是一个无法确保线程安全的实现结构发生变化,或者即使有任何修改,或者根本没有。并且初始人口 is 发生变化,因此它可能会破坏线程安全性,谁知道多久。 Collectors 甚至提供专门的 toConcurrentMap() 方法来解决这个问题 - 所以,至少,我应该改用它。
但即使我在#2 中使用toConcurrentMap(),我也无法将我的字段设为final,因为那样我将无法在setter 中对其进行初始化。所以这是我的选择:
a) 在自动装配的构造函数中初始化并填充Map,坦率地说,我更喜欢这样。但是很少有团队这样做,那么如果我们放弃该解决方案怎么办?还有哪些其他选择?
b) 将 Map 初始化为空的 final ConcurrentHashMap,然后将其填充到 setter 中。这是可能的,但我们必须先list.forEach() 然后map.put()。这看起来仍然是 Java 6;或者我们绝对可以做map.addAll(list....toMap()),但它对Map 的重复是无用的,即使是临时的。
c) 在现场使用volatile。稍微降低性能而没有任何需要,因为在某些时候该字段永远不会改变。
d) 使用synchronized 访问该字段并读取其值。显然比 (c) 还要糟糕。
此外,这些方法中的任何一种都会使读者认为代码实际上需要对Map 进行一些多线程读取/写入,而实际上,它只是多线程读取。
那么,当一个理性的大师想要这样的东西时,他们会怎么做?
此时,最好的解决方案似乎是具有volatile 字段的解决方案,使用toConcurrentMap 在setter 中分配。有更好的吗?或者我只是在编造没有人真正遇到过的问题?
【问题讨论】:
-
So, what does reasonable guru do when they want something like that?-- 你的答案是:synchronized。一段时间后,如果有时间,有人可能会将其更改为某种有效的锁定机制,例如StampedLock,甚至手动、顺序、不同步的ConcurrentHashMap填充 - 这是经典且经过验证的......虽然与流人口相比可能不那么“美丽”。真正的软件工程师并不总是关心美观——可读性、可维护性和稳定性更为重要 -
AFAIR(但我找不到任何参考),Spring 确保有一个发生在之前,即 bean 的注入发生在它们使用之前。应该没有什么可担心的。而且我不知道有任何地图(当然也不是 Collectors.toMap() 使用的 HashMap),当您只读取它时,它本身会决定更改其内部结构。也就是说,我确实更喜欢构造函数注入而不是 setter 注入。
-
我完全错过了这个问题是关于二传手注入和弹簧单例的事实。那么在这种情况下:是的,这实际上并不重要,因为注入只发生 每个单例 bean 一次
-
"only once" - 无关紧要,我不在乎它发生了多少次,我在乎结果是否被每个人观察到。即使某些事情发生在很久以前,如果没有适当的发生之前,某些线程可能还没有观察到它。 JB Nizet 的回答说 Spring 可以确保这一点 - 我也听说过,我会尝试找到具体细节。
-
" 当你只读取它时,它自己决定改变它的内部结构。" - 不,当然不是,我试图解释说,当它被填充时,它可能并且会改变它的结构,并且由于它不是线程安全的,没有人可以确定进一步阅读它会很好。线程安全不仅意味着“在并发访问中”,而且更普遍 - 来自并发线程的访问。因此 volatile.
标签: java spring multithreading