调用ConcurrentMap.remove()
之前的某个帖子中的操作是否保证在看到从另一个线程中删除后的操作之前发生?
Documentation说明了关于置于集合中的对象:
在将对象放入任何并发集合之前的线程中的操作发生在从另一个线程中的集合访问或删除该元素之后的操作之前。
示例代码:
{
final ConcurrentMap map = new ConcurrentHashMap();
map.put(1, new Object());
final int[] value = { 0 };
new Thread(() -> {
value[0]++;
value[0]++;
value[0]++;
value[0]++;
value[0]++;
map.remove(1); // A
}).start();
new Thread(() -> {
if (map.get(1) == null) { // B
System.out.println(value[0]); // expect 5
}
}).start();
}
与 B 的发生在&#em>之前的 A ?因此,如果该程序只打印5?
答案 0 :(得分:4)
您已经发现这些并发工具的一个有趣的细微方面很容易被忽视。
首先,不可能提供关于删除和检索null
引用的一般保证,因为后者只能证明映射的缺席,而不是之前的删除,即线程可以在键有映射之前读取映射的初始状态,当然,这不能建立发生之前关系的动作发生在地图构建之后。
此外,如果有多个线程删除相同的密钥,则在检索null
时,您无法假设发生在之前的关系,因为您不知道哪个删除了已经完成。此问题类似于两个线程插入相同值的情况,但后者可以通过仅执行可区分值的插入或通过遵循对值对象执行所需修改的常规模式在应用程序端修复要插入并仅查询检索到的对象。对于删除,没有这样的修复。
在您的特殊情况下,map.put(1, new Object())
操作与第二个线程的开始之间存在发生之前的关系,因此如果第二个线程在查询时遇到null
关键1
,很明显它见证了你的代码的唯一删除,但规范并没有为这个特殊情况提供明确的保证。
相反,Java 8’s ConcurrentHashMap
的规范说,
检索反映了最近已完成更新操作的结果。 (更正式地说,给定密钥的更新操作带有发生 - 之前关系与该密钥的任何(非空)检索报告更新的值。)
明确排除null
次检索。
我认为,使用当前(Java 8)ConcurrentHashMap
实现,您的代码不会因为它以volatile
语义执行对其内部支持数组的所有访问而相当保守而破坏。但这只是当前的实现,并且如上所述,您的代码是一个特殊情况,并且可能会因为对现实应用程序的每次更改而破坏。
答案 1 :(得分:1)
由于ConcurrentHashMap是一个线程安全集合,因此语句map.remove(1)
必须具有读屏障和写屏障(如果它改变了映射)。表达式map.get(1)
必须具有读屏障或一个,或者这两个操作都不是线程安全的。
实际上,ConcurrentHashMap直到Java 7,使用分区锁,因此它几乎每个操作都有一个读/写屏障。
ConcurrentSkipListMap不必使用锁,但要执行任何线程安全写操作,需要写入屏障。
这意味着您的测试应始终按预期进行。
答案 2 :(得分:1)
不,你的订单错了。
从put()
到后续get()
的发生在之前的边缘。该边缘不对称,并且在另一个方向上不起作用。从get()
到另一个get()
或remove()
,或从put()
到另一个put()
,发生前边缘没有发生}。
在这种情况下,您将对象放入地图中。然后修改另一个对象。这是禁忌。在第二个线程中对get()
的写入没有任何优势,因此第二个线程可能看不到这些写入。
在英特尔硬件上,我认为这将始终有效。但是,Java内存模型无法保证,因此如果您将此代码移植到不同的硬件上,则必须要小心。
答案 3 :(得分:1)
A不需要在B之前发生。
只有原始put
在两者之前发生。因此,B处的空值表示A发生了。
但是没有提到回写线程本地内存缓存和++
和remove
的指令顺序。 volatile
未使用;而是使用Map和数组来有希望保持线程数据同步。在写回数据时,有序关系应该再次保持。
根据我的理解,A可以移除并被写回,然后最后的++发生,并且在B处打印出类似于4的内容。我会将volatile
添加到数组中。地图本身会很好。
我很不确定,但由于我没有看到相应的答案,我伸出了脖子。 (要自学。)