尽量减少JDK8 ConcurrentHashMap检查的锁定范围

1。

我有多个线程更新ConcurrentHashMap。 每个线程将一个整数列表附加到基于密钥的地图条目的值中。 任何线程都没有删除操作。

这里的重点是我想尽可能地减少锁定和同步的范围。

我看到computeIf ...()方法的doc说:“在计算正在进行时,其他线程在此映射上的某些尝试更新操作可能会被阻止”,这并不令人鼓舞。 另一方面,当我查看它的源代码时,我没有看到它在整个地图上锁定/同步的位置。

因此,我想知道使用computeIf ...()和下面的本土“方法2” 的理论性能比较

2。

另外,我觉得我在这里描述的问题可能是您可以在ConcurrentHashMap上执行最简单的check-n-set(或通常是“复合”)操作之一

然而,我并不十分自信, 也无法找到很多关于如何在ConcurrentHashMap上完成这种简单的复合操作的指导, 而无需在整个地图上进行锁定/同步

所以任何一般的良好做法建议,将非常感激。

public void myConcurrentHashMapTest1() {

    ConcurrentHashMap<String, List<Integer>> myMap = new ConcurrentHashMap<String, List<Integer>>();

    // MAP KEY: a Word found by a thread on a page of a book 
    String myKey = "word1";

    // -- Method 1: 
    // Step 1.1 first, try to use computeIfPresent(). doc says it may lock the
    //      entire myMap. 
    myMap.computeIfPresent(myKey, (key,val) -> val.addAll(getMyVals()));
    // Step 1.2 then use computeIfAbsent(). Again, doc says it may lock the
    //      entire myMap. 
    myMap.computeIfAbsent(myKey, key -> getMyVals());    
}

public void myConcurrentHashMapTest2() {        
    // -- Method 2: home-grown lock splitting (kind of). Will it theoretically 
    //      perform better? 

    // Step 2.1: TRY to directly put an empty list for the key
    //      This may have no effect if the key is already present in the map
    List<Integer> myEmptyList = new ArrayList<Integer>();
    myMap.putIfAbsent(myKey, myEmptyList);

    // Step 2.2: By now, we should have the key present in the map
    //      ASSUMPTION: no thread does removal 
    List<Integer> listInMap = myMap.get(myKey);

    // Step 2.3: Synchronize on that list, append all the values 
    synchronized(listInMap){
        listInMap.addAll(getMyVals());
    }

}

public List<Integer> getMyVals(){
    // MAP VALUE: e.g. Page Indices where word is found (by a thread)
    List<Integer> myValList = new ArrayList<Integer>(); 
    myValList.add(1);
    myValList.add(2);

    return myValList;
}

你基于你的假设(根据预期使用ConcurrentHashMap对你来说太慢了)对Javadoc的误解。 Javadoc没有说明整个地图将被锁定。 它也没有声明每个computeIfAbsent()操作执行悲观锁定。

实际上可以锁定的是一个bin(aka bucket),它对应于ConcurrentHashMap内部数组支持的单个元素。 请注意,这不是包含多个存储桶的Java 7映射段。 当这种垃圾箱被锁定时,可能被阻止的操作仅仅是散列到同一垃圾箱的密钥的更新。

另一方面,您的解决方案并不意味着避免了ConcurrentHashMap中的所有内部锁定 - computeIfAbsent()只是可以降级以在更新时使用synchronized块的一种方法。 即使你最初为某个键放置一个空列表的putIfAbsent() ,如果它没有碰到一个空的bin,它也会被阻塞。

但更糟糕的是,您的解决方案不能保证您的synchronized批量更新的可见性。 你可以保证get()发生在putIfAbsent()之前,它在你的批量更新和随后的get()之间观察到的值,但没有发生。

PS您可以在其OpenJDK实现中进一步了解ConcurrentHashMap中的锁定:http://hg.openjdk.java.net/jdk8/jdk8/jdk/file/687fd7c7986d/src/share/classes/java/util/concurrent/ConcurrentHashMap .java,第313-352行。


正如Dimitar Dimitrov所解释的, compute…方法通常不会锁定整个地图。 在最好的情况下,即不需要增加容量并且不存在散列冲突,只锁定单个密钥的映射。

然而,你仍然可以做得更好:

  • 一般来说,避免执行多个查找。 这适用于两种变体,分别使用computeIfPresentcomputeIfAbsent ,以及使用putIfAbsent后跟get
  • 仍然建议最大限度地减少持有锁时执行的代码,即不要在持有锁时调用getMyVals() ,因为它不依赖于地图的状态
  • 综合起来,更新应该如下所示:

    // compute without holding a lock
    List<Integer> toAdd=getMyVals();
    // update the map
    myMap.compute(myKey, (key,val) -> {
        if(val==null) val=toAdd; else val.addAll(toAdd);
        return val;
    });
    

    要么

    // compute without holding a lock
    List<Integer> toAdd=getMyVals();
    // update the map
    myMap.merge(myKey, toAdd, (a,b) -> { a.addAll(b); return a; });
    

    这可以简化为

    myMap.merge(myKey, getMyVals(), (a,b) -> { a.addAll(b); return a; });
    
    链接地址: http://www.djcxy.com/p/91899.html

    上一篇: minimizing lock scope for JDK8 ConcurrentHashMap check

    下一篇: ConcurrentHashMap, which concurrent features improved in JDK8