不变的映射-如果两个线程插入相同的密钥怎么办?

时间:2018-11-05 18:14:28

标签: multithreading scala immutability

这是我对不可变数据结构(尤其是不可变映射)的了解,如果我错了,请纠正我:

  1. 更新时,它们不会改变内部结构,但会创建包含更新数据的副本版本。
  2. 它们通常由树状数据结构实现,因为复制树枝比复制线性列表要有效得多。
  3. 当有许多线程正在使用它们时,这些线程之间的状态将不同步。一些线程可能正在使用旧版本,但阅读线程不必等待更新线程完成。
  4. 至少需要一个锁才能将分配同步到引用数据结构的共享变量(但在Java中,分配是原子的,所以这不是问题)。

如果有两个线程t1t2向不变映射插入相同的键。在t1完成其工作并更新共享引用之前,t2开始其工作,并接收尚未插入其密钥的旧地图,并继续插入相同的密钥。现在,映射的最终版本取决于最后更新共享引用的线程。在这两种情况下,都有一个废弃的键值,垃圾回收器会很快清除该键值。

我的问题是,当两个线程将相同的键插入一个不变的映射(该引用在线程之间共享)时,如何解决该问题?还是首先使用共享引用是错误的?

2 个答案:

答案 0 :(得分:3)

关于术语:有两种集合称为“不可变的”。

  1. 在尝试调用mutator操作时抛出异常的集合(例如Guava的ImmutableMap)。
  2. 在调用mutator时创建并返回其自身的新版本,而不是更改共享状态(例如写时复制)的集合。

您可能正在问第二种“不变性”。

我想你有这样的东西:

private CopyOnWriteTree tree;

void insertValue(Value value) {
    tree = tree.insert();
}

在这种情况下,使用共享引用似乎没有用。

如果由多个线程使用对包含insertValue()字段的对象的相同共享引用来调用tree,那么最好的办法就是使用{{ 1}},synchronizedvolatile,但其中之一将丢失。 (请注意,这里不是参考更新原子性;问题是没有这种同步,甚至不能保证一个线程能够看到其他线程所做的更新。)

写时复制集合适用于每个线程处理自己的数据副本并在工作完成后将其丢弃的情况。

如果您需要同时保留两个更新,请不要使用写时复制集合。而是尝试一些并发集合(AtomicReference或类似的东西)。

PS。我刚刚意识到问题出在Scala集合上,而我的回答就像是关于Java。尽管如此,逻辑仍然相同,内存模型也相同。

答案 1 :(得分:2)

问题分为两部分。第一个问题是

  

如果两个线程写入相同的共享引用会发生什么?

答案是它可能会损坏,您不应该这样做。如果要让两个线程写入相同的引用,则需要使用org.springframework.messaging.MessageDeliveryException: failed to send Message to channel 之类的同步机制来保护它。

您需要问的第二个问题是

  

两个线程同时读取一个不变的对象总是安全的吗?

答案是否定的。

不可变对象可能包含可变数据,因此可能不是线程安全的。您需要查看有关对象的规范,以确定它是否是线程安全的。