【问题标题】:minimizing lock scope for JDK8 ConcurrentHashMap check-and-set operation最小化 JDK8 ConcurrentHashMap 检查和设置操作的锁定范围
【发布时间】:2016-04-14 21:12:50
【问题描述】:

1.

我有多个线程更新 ConcurrentHashMap。 每个线程根据键将整数列表附加到映射条目的值。 没有任何线程的删除操作。

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

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

因此,我想知道使用 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;
}

【问题讨论】:

  • 一般来说,它不会锁定整个地图,而只是地图的segment
  • 锁定是悲观的,而不是乐观的,因此您应该使用 get 预先筛选这两种方法。
  • @LouisWasserman 谢谢!如果你也在谈论 JDK8,例如computeIfAbsent(),我假设你指的是 for {... else{...synchronized (f)...}} 中的内容?
  • @BenManes 谢谢!但抱歉,当你说“你应该用 get 预先筛选这两种方法”时,我不太明白
  • @Stochastika “读取,如果不存在,则计算如果不存在”避免锁定 bin 以确定条目是否存在。相反,大多数读取将在未找到和计算时找到具有少量锁定的现有值。这个可以大大improve performance

标签: java multithreading java-8 concurrenthashmap atomic


【解决方案1】:

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

实际上可以锁定的是一个 bin(也称为存储桶),它对应于 ConcurrentHashMap 的内部数组支持中的单个元素。请注意,这不是 Java 7 的包含多个存储桶的地图段。当这样的 bin 被锁定时,可能被阻止的操作只是对哈希到同一 bin 的键的更新。

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

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

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

【讨论】:

  • 谢谢迪米塔!我一定会通读 OpenJDK 文档和代码。 1. 你的意思是即使对于当前线程,我的第二个方法中的“listInMap”仍然可以是null,就像你评论的最后一段一样? 2.当你提到“降级......”时,它是否像computeIfAbsent()中的部分:'for {... else{...synchronized(f)...}}' 在 Oracle JDK8 中? 3. 事实上,我对那个代码有点困惑。对我来说,即使在发现条目 NOT Absent 之后,它也几乎看起来像是在做一些工作,这与 'computeIfAbsent' 有点矛盾?
  • 1.使用空列表调用putIfAbsent() 的线程稍后无法看到该键的null 值(除非发生并发remove())。但是,使用您的方案,稍后为同一键执行 get() 的其他线程可能每次都只看到空列表,而看不到您添加的内容。在实践中,对于强大的内存模型架构,这种情况可能很少发生,但它仍然是一个有效的问题。 2. 是的,没错。 3. 您正在混合存在散列到同一个 bin 的项目,以及具有完全相同键的项目。看看onlyIfAbsent 标志是如何使用的。
  • 谢谢迪米塔!赞成评论并标记答案。
【解决方案2】:

正如explained by Dimitar Dimitrov 一样,compute… 方法通常不会锁定整个地图。在最好的情况下,即不需要增加容量并且没有哈希冲突,只有单个键的映射被锁定。

但是,您仍然可以做得更好:

  • 通常,避免执行多次查找。这适用于两种变体,使用computeIfPresent 后跟computeIfAbsent,以及使用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; });

【讨论】:

  • 谢谢霍尔格!赞成。从今天开始,merge() 就在我最喜欢的列表中。
  • 还有一个问题,@Holger,关于我在原始问题中的第 2 点,就 ConcurrentHashMap 这种复合动作的一般方法而言,对于您上面提出的解决方案,它也应该适用于即使其他线程可能正在从地图中删除条目,情况如何?
  • 特别是在移除的时候,它工作很顺利。请注意,当您执行普通的get 时,您将获得对存储列表的引用,并且为了使用该列表,您需要采取不同的措施来防止并发更新(例如,当您确定所有编写器时,您可以这样做线程已完成)。相反,通过remove 检索列表保证不会有并发更新,因为后续更新操作将创建一个新列表。
猜你喜欢
  • 2013-05-06
  • 1970-01-01
  • 2018-03-29
  • 1970-01-01
  • 1970-01-01
  • 2015-08-31
  • 1970-01-01
  • 2018-06-22
  • 1970-01-01
相关资源
最近更新 更多