Java ConcurrentHashMap.computeIfPresent值修改可见性

时间:2018-09-30 11:39:57

标签: java multithreading concurrenthashmap memory-visibility

比方说,我有一个以集合为值的并发映射:

Map<Integer, List<Integer> map = new ConcurrentHashMap<>();
map.putIfAbsent(8, new ArrayList<>());

我将值更新如下:

map.computeIfPresent(8, (i, c) -> {
    c.add(5);
    return c;
});

我知道computeIfPresent整个方法调用是原子执行的。但是,考虑到该映射是由多个线程同时访问的,我有点担心对基础集合所做的修改的数据可见性。在这种情况下,在调用map.get

后将在列表中看到值5

我的问题是,如果在map.get方法调用内进行了更改,则在调用computeIfPresent时,列表将在其他线程中可见。

请注意,我知道如果在执行更新操作之前引用列表,将看不到列表的更改。我不确定如果在更新操作后引用了列表(通过调用map.get)来引用列表,该列表的更改是否可见。

我不确定如何解释文档,但在我看来,在这种特殊情况下,事前发生的关系将保证对底层集合所做更改的可见性

  

更正式地说,给定键的更新操作与该键报告更新值的任何(非空)检索具有先发生后关联的关系

3 个答案:

答案 0 :(得分:4)

该方法记录为atomic的事实对visibility并没有多大意义(除非这是文档的一部分)。例如,使其更简单:

// some shared data
private List<Integer> list = new ArrayList<>();

public synchronized void addToList(List<Integer> x){
     list.addAll(x);
}

public /* no synchronized */ List<Integer> getList(){
     return list;
}

我们可以说addToList确实是原子的,一次只能调用一个线程。但是一旦某个线程调用getList-根本就无法保证visibility(因为要建立it has to happens on the same lock)。因此,可见性是在关注之前发生的事情,computeIfPresent文档对此没有任何说明。

相反,类文档说:

  

检索操作(包括获取)通常不会阻塞,因此可能与更新操作(包括放置和删除)发生重叠

这里的关键点显然是 overlap ,因此其他一些调用get的线程(因此获得了List的持有权)可以看到List在某种状态下不一定是computeIfPresent开始的状态(在您实际呼叫get之前)。请务必进一步阅读以了解 some 的实际含义。

现在到该文档中最棘手的部分:

  

检索反映了最近发生的已完成更新操作的结果。更正式地说,给定键的更新操作与 happens-before 关系与该键的任何(非空)检索报告更新值。

再次阅读有关 completed 的那句话,它的意思是,当线程执行get时,您唯一能读到的是最后完成的状态List在里面。现在下一个句子说在两个动作之间建立了之前

考虑一下,在两个随后的动作之间建立了一个happens-before(例如上面的同步示例);因此,在内部,当您更新Key时,可能会出现易失的书面信号,表明更新已完成(我很确定它不是以这种方式完成的,只是一个例子)。为了在实际工作之前发生这种情况,get必须读取该易失性并查看写入其中的状态。如果看到该状态,则表示发生在之前;而且我猜想是通过其他一些方法实际上可以实施的。

因此,为回答您的问题,所有调用get的线程都将看到该键上发生的last completed action;在您的情况下,如果您可以保证该订单,我会说,是的,它们将可见。

答案 1 :(得分:2)

c.add(5)不是线程安全的,c的内部状态不受映射的保护。

使单个值和插入/删除组合线程安全且免于竞争条件的确切方法取决于使用模式(同步包装,写时复制,无锁队列等)。

答案 2 :(得分:1)

为澄清您的问题:

您要提供一些外部保证,以使Map.computeIfPresent()先被调用 Map.get()

您尚未说明如何执行此操作,但可以说您是通过使用JVM提供的具有 happens-before 语义的功能来执行此操作的。如果是这种情况,那么仅通过 happens-before的 association 单独保证List.add()对于调用Map.get()的线程是可见的关系。

现在要回答您实际上要问的问题:如前所述,更新操作ConcurrentHashMap.computeIfPresent()与访问方法{的后续调用之间存在先于关系{1}}。当然,ConcurrentMap.get()List.add()的末尾之间存在先发生关系。

放在一起,答案是

可以保证其他线程在通过ConcurrentHashMap.computeIfPresent()获得的5中看到List已提供,您保证Map.get()实际上是之后Map.get()结束时调用(如问题所述)。如果后一个保证无效,并且在computeIfPresent()结束之前以某种方式调用了Map.get(),则由于computeIfPresent()不是线程安全的,因此无法保证其他线程会看到什么。