14

假设我有一个以集合为值的并发映射:

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

我更新值如下:

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

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

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

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

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

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

4

3 回答 3

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(因为要建立它,它必须发生在同一个锁上)。所以可见性是在关注之前发生的事情,computeIfPresent文档根本没有说明这一点。

相反,类文档说:

检索操作(包括 get)一般不会阻塞,因此可能与更新操作(包括 put 和 remove)重叠。

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

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

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

再次阅读关于完成的那句话,它说的是,当线程执行时,您唯一可以阅读的是 Listget所处的最后一个完成状态。现在下一句说在两个动作之间建立之前发生了一个事件。

Think about it, a happens-before is established between two subsequent actions (like in the synchronized example above); so internally when you update a Key there could be a volatile written signaling that update has finished (I am pretty sure it's not done this way, just an example). For the happens before to actually work, get has to read that volatile and see the state that was written to it; if it sees that state it means that happens before has been established; and I guess that by some other technique this is actually enforced.

So to answer your question, all the threads calling get will see the last completed action that happened on that key; in your case if you can guarantee that order, I'd say, yes, they will be visible.

于 2018-09-30T20:14:05.333 回答
2

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

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

于 2018-09-30T11:54:05.963 回答
1

To clarify your question:

You're providing some external guarantee such that Map.computeIfPresent() is called before Map.get().

You haven't stated how you're doing this, but let's say you're doing it by using something with happens-before semantics provided by the JVM. If this is the case then that alone guarantees List.add() is visible to the thread calling Map.get() simply by association of the happens-before relationship.

Now to answer the question you're actually asking: As you've noted, there's a happens-before relation between update operation ConcurrentHashMap.computeIfPresent() and the subsequent calling of the access method ConcurrentMap.get(). And naturally, there's a happens-before relation between List.add() and the end of ConcurrentHashMap.computeIfPresent().

Put together, answer is yes.

There's a guarantee the other thread will see 5 in the List obtained through Map.get(), provided you're guaranteeing Map.get() is actually called after computeIfPresent() ends (as stated in the question). If the latter guarantee goes foul and Map.get() is somehow called before computeIfPresent() ends, there's no guarantees to what the other thread will see since ArrayList is not thread-safe.

于 2018-10-01T02:46:07.277 回答