说我有一张确保线程安全(检索完全并发)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
是否有问题?有什么方案可能无法按预期执行吗?
答案 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
我希望对您有帮助