Java ConcurrentHashMap.computeIfPresent 值修改可见性

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

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

后会在列表中看到值 5

我的问题是,如果更改是在 computeIfPresent 方法调用中执行的,那么在调用 map.get 时列表是否会在其他线程中可见。

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

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

More formally, an update operation for a given key bears a happens-before relation with any (non-null) retrieval for that key reporting the updated value

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

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

该方法被记录为 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 文档根本没有说明这一点。

相反,class 文档说:

Retrieval operations (including get) generally do not block, so may overlap with update operations (including put and remove).

这里的关键点显然是overlap,因此一些其他线程调用get(从而掌握了List),可以看到List 在某些州;不一定是 computeIfPresent 开始的状态(在您实际调用 get 之前)。请务必进一步阅读以了解 some 实际上可能意味着什么。

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

Retrievals reflect the results of the most recently completed update operations holding upon their onset. More formally, an update operation for a given key bears a happens-before relation with any (non-null) retrieval for that key reporting the updated value.

再读一遍关于 completed 的那句话,它说的是当线程执行 get 时你唯一能读到的是 last已完成 状态 List 所在的状态。现在下一句话说在两个动作之间建立了 happens before

想一想,在两个后续动作之间建立了一个happens-before(就像上面的同步示例);所以在内部,当你更新一个 Key 时,可能会有一个不稳定的书面信号表明更新已经完成(我很确定它不是这样完成的,只是一个例子)。为了让 happens before 真正起作用,get 必须读取那个 volatile 并查看写入它的状态;如果它看到该状态,则意味着 发生在 之前;我想这实际上是通过其他一些技术强制执行的。

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

澄清你的问题:

您正在提供一些外部保证,以便 Map.computeIfPresent() 在 之前被调用 Map.get().

您没有说明您是如何做到这一点的,但假设您是通过使用 JVM 提供的具有 happens-before 语义的东西来做到这一点的。如果是这种情况,那么仅通过 association 就可以保证 List.add() 对调用 Map.get() 的线程可见发生在关系之前。

现在回答您实际提出的问题:正如您所指出的,更新操作 ConcurrentHashMap.computeIfPresent() 与后续调用之间存在 happens-before 关系访问方法 ConcurrentMap.get()。当然,在 List.add()ConcurrentHashMap.computeIfPresent().

的结尾之间存在 happens-before 关系

放在一起,答案是

保证其他线程将在通过 Map.get() 获得的 List 中看到 5提供 你保证 Map.get() 实际上是 computeIfPresent() 结束后调用(如问题中所述)。如果后一个保证出错并且 Map.get()computeIfPresent() 结束之前以某种方式被调用,则无法保证其他线程将看到什么,因为 ArrayList 不是线程安全的。