我正在阅读sentinel的源代码,我发现当Map需要添加一个条目时,它会创建一个新的hashmap来替换旧的而不是使用map.put directly.像这样:
public class NodeSelectorSlot extends AbstractLinkedProcessorSlot<Object> {
private volatile Map<String, DefaultNode> map = new HashMap<String, DefaultNode>(10);
@Override
public void entry(Context context, ResourceWrapper resourceWrapper, Object obj, int count, boolean prioritized, Object... args)
throws Throwable {
DefaultNode node = map.get(context.getName());
if (node == null) {
synchronized (this) {
node = map.get(context.getName());
if (node == null) {
node = new DefaultNode(resourceWrapper, null);
// create a new hashmap
HashMap<String, DefaultNode> cacheMap = new HashMap<String, DefaultNode>(map.size());
cacheMap.putAll(map);
cacheMap.put(context.getName(), node);
map = cacheMap;
((DefaultNode) context.getLastNode()).addChild(node);
}
}
}
context.setCurNode(node);
fireEntry(context, resourceWrapper, node, count, prioritized, args);
}
...
}
它们之间有什么不同?
2条答案
按热度按时间xmjla07d1#
您正在查看的代码是从Map中获取一个节点,如果没有节点,则创建并添加一个新的
Node
。显然,这个操作需要是线程安全的。实现这个操作的简单方法是:
get
和put
操作。ConcurrentHashMap
,它具有原子地完成这种事情的操作;例如computeIfAbsent
。这段代码的作者选择了一种不同的方法。他们使用所谓的双重检查锁定(DCL)来避免在持有锁时执行初始
get
。这就是这段代码的作用:作者决定,当他们需要向Map中添加新条目时,他们需要用一个新的Map替换整个Map。从表面上看,这 * 似乎 * 是不必要的。Map更新是在持有锁的情况下执行的,
volatile
添加了一个 happens before,* 似乎 * 是为了确保初始的map.get
调用看到最近对HashMap的任何写入。问题是在获取
map
引用和get
调用完成之间有一个很小的时间窗口,同时执行的put
操作可能正在更新HashMap
数据结构。这是有害的,因为这些更改可能导致get
读取过时数据(因为从put
写入到get
读取之间没有 * 发生在 * 之前的关系)。更糟糕的是,put
可能会触发散列链的重构,甚至是散列数组的扩展。(至少)在HashMap
规范之外,因为HashMap
没有定义为线程安全的。作者的解决方案是用现有条目和新条目创建一个新的
HashMap
,然后用一个赋值更新map
。我没有做过正式的分析,但我认为这种方法是线程安全的。简而言之,代码创建一个新的
HashMap
的原因是为了使DCL方法线程安全。如果忽略线程安全方面,这种方法在功能上等同于简单的
put
。最后,我们需要考虑作者的方法是否能给予最佳性能。答案将取决于缓存条目的数量是否稳定,以及它是否相对较小。一个观察结果是,该高速缓存添加
N
条目的成本是O(N^2)
!!(假设条目永远不会被删除,就像看起来的那样。)fykwrbwg2#
这就是所谓的copy-on-write,旨在确保线程安全。当读操作比写操作多得多时,它比
ConcurrentHashMap
这样的机制更有效。参考:https://github.com/alibaba/Sentinel/issues/1733