值可变的并发安全容器的线程安全

时间:2018-10-11 11:13:36

标签: java concurrency nested thread-safety

说我有一张确保线程安全(检索完全并发)ConcurrentHashMap<String, Foo>的映射,其中要检索的值是一些 mutable 对象

class Foo { 
    public Object bar;
}

Foo值只能由单个线程构造,仅添加一次到映射,然后再不修改。该操作可能如下所示:

Foo foo = new Foo();
foo.bar = "Test";
concurrentMap.add("key", foo);

在单独的线程中,通过查看映射中的值来执行工作(假设此处的值是事先正确设置的)

System.out.println(concurrentMap.get("key").bar);

在这种情况下访问Foo::bar是否有问题?有什么方案可能无法按预期执行吗?

2 个答案:

答案 0 :(得分:1)

它应该起作用,因为在并发哈希表中放入值和从中退出应该建立先发生后关系(请参阅Is ConcurrentHashMap.get() guaranteed to see a previous ConcurrentHashMap.put() by different thread?)。使用者线程应看到插入地图之前的柱线值。

我个人会尽力使Foo(和bar)对象不可变(为什么它们永远不变,为什么不这样呢?)。

答案 1 :(得分:1)

按定义,只读操作是线程安全的。仅当至少有一个线程修改数据时,才会发生争用条件或数据争用。

因此,如果将值放入映射中,则在创建从映射中读取值的线程之前,该操作是完全安全的。而且,如果您从未修改过bar或它在foo中的引用,那么您也应该没事。在这种情况下,您甚至不需要并发映射。普通地图就可以。

但是,如果修改bar或在foo中修改bar的引用,您可能会得到意外的结果。这是可能出问题的示例。假设bar是long

class Foo { 
    public long bar;
}

您正在执行线程1:

Foo foo = concurrentMap.get("key");
.....
..... /// some code
System.out.println(foo.bar);

后台还有另一个线程可以做到这一点:

Foo foo = concurrentMap.get("key");
.....
long newBar = foo.bar + 1;
foo.bar = newBar;

在这里,您的比赛条件很正常。

现在,如果线程2实际上只是这样做:

 Foo foo = concurrentMap.get("key");
 .....
 long newBar = rand.nextLong();
 foo.bar = newBar;

您没有争用条件,但是您有数据争用,因为long是64位,并且编译器可能会执行对long和double的两次赋值操作。

还有很多可能会出错的情况,如果不非常小心,就很难对它们进行推理

因此至少,您应该使bar变得不稳定,如下所示:

class Foo { 
    public volatile Object bar;
}

如果它是某个类的实际对象,请非常小心如何操作bar及其内部内容。

如果您想了解有关数据竞赛的更多信息并更深入地了解竞赛条件,则应该查看这一出色的课程https://www.udemy.com/java-multithreading-concurrency-performance-optimization/?couponCode=CONCURRENCY

其中有一部分对此进行了很好的说明,并提供了很好的示例。

您还应该查看Java内存模型官方文档https://docs.oracle.com/javase/specs/jls/se7/html/jls-17.html#jls-17.4

我希望对您有帮助