java map.put和创建一个新的map有什么不同?

7kqas0il  于 2022-11-27  发布在  Java
关注(0)|答案(2)|浏览(164)

我正在阅读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);
    }
...
}

它们之间有什么不同?

xmjla07d

xmjla07d1#

您正在查看的代码是从Map中获取一个节点,如果没有节点,则创建并添加一个新的Node
显然,这个操作需要是线程安全的。实现这个操作的简单方法是:

  • 锁定Map,并在保持锁定的同时执行getput操作。
  • 使用一个ConcurrentHashMap,它具有原子地完成这种事情的操作;例如computeIfAbsent

这段代码的作者选择了一种不同的方法。他们使用所谓的双重检查锁定(DCL)来避免在持有锁时执行初始get。这就是这段代码的作用:

DefaultNode node = map.get(context.getName());
    if (node == null) {
        synchronized (this) {
            node = map.get(context.getName());
            ...

作者决定,当他们需要向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)!!(假设条目永远不会被删除,就像看起来的那样。)

fykwrbwg

fykwrbwg2#

这就是所谓的copy-on-write,旨在确保线程安全。当读操作比写操作多得多时,它比ConcurrentHashMap这样的机制更有效。
参考:https://github.com/alibaba/Sentinel/issues/1733

相关问题