Java ConcurrentHashMap.computeIfPresent value modification visibility

假设我有一个并发地图,集合作为值:

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

我更新了值,如下所示:

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

我知道整个方法调用都是以原子方式执行的。但是,考虑到此映射由多个线程同时访问,我有点担心对基础集合所做的修改的数据可见性。在这种情况下,调用后,值 5 将在列表中显示computeIfPresentmap.get

我的问题是,如果在方法调用中执行更改,则在调用时,是否会在其他线程中显示对列表的更改。map.getcomputeIfPresent

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

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

更正式地说,给定密钥的更新操作与报告更新值的该密钥的任何(非空)检索都具有发生之前关系


答案 1

该方法被记录为 的事实并不意味着什么(除非这是文档的一部分)。例如,为了简化此操作:atomicvisibility

// 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;
}

我们可以说这确实是原子的,一次只有一个线程可以调用它。但是一旦某个线程调用 - 根本无法保证(因为要建立它,它必须发生在同一个锁上)。因此,可见性是之前发生的事情,文档根本没有对此有任何说明。addToListgetListvisibilitycomputeIfPresent

相反,类文档说:

检索操作(包括get)一般不阻塞,因此可能与更新操作(包括放置和删除)重叠

这里的关键点显然是重叠的,所以其他一些线程调用(从而掌握它),可以在某种状态下看到它;不一定是开始的状态(在您实际调用之前)。请务必进一步阅读以了解某些内容的实际含义。getListListcomputeIfPresentget

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

检索反映最近完成的更新操作在其开始时保持的结果。更正式地说,给定密钥的更新操作与报告更新值的该密钥的任何(非空)检索都具有发生之前关系。

再次阅读关于完成的那句话,它说的是,当线程这样做时,您唯一可以阅读的是List处于的最后一个完成状态。现在下一句话说,在两个动作之间建立之前有一个发生。get

想想看,在两个后续操作之间建立了a(如上面的同步示例);因此,在内部,当您更新一个时,可能会有一个不稳定的书面信号,表明更新已经完成(我很确定它不是以这种方式完成的,只是一个例子)。对于实际工作之前发生的事情,必须阅读该不稳定并查看写入它的状态;如果它看到该状态,则意味着在已经建立之前发生;我猜想通过其他一些技术,这实际上是强制执行的。happens-beforeKeyget

因此,为了回答您的问题,所有线程调用都将看到该键上发生的事情;在你的情况下,如果你能保证这个顺序,我会说,是的,他们会是可见的。getlast completed action


答案 2

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

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