包装不可变集合并在Scala中维护线程安全性?

时间:2016-09-16 06:21:18

标签: multithreading scala concurrency

我想创建一个使用Scala Map作为后备存储的线程安全容器。我宁愿只公开其方法的一个子集,而不是将用户暴露给底层Map。

示例可能类似于以下内容......

class MyContainer[A] {

  def add(thing: A): Unit = {
    backingStore = backingStore + (thing.uuid -> thing)
  }

  def filter(p: A => Boolean): Option[Iterable[A]] = {
    val filteredThings = backingStore.values.filter(p)
    if (filteredThings.isEmpty) None else Some(filteredThings)
  }

  def remove(uuid: UUID): Option[A] = backingStore.get(uuid) match {
    case optionalThing @ Some(thing) =>
      backingStore = backingStore - uuid; optionalThing
    case None => None
  }

  @ volatile private[this] var backingStore = immutable.HashMap.empty[UUID, A]

}

...我怀疑即使底层后备存储是不可变的并且它的引用是volatile,容器也不是线程安全的。

假设我有两个独立的线程运行,可以访问上述容器的实例。线程1过滤底层集合并获得一些结果;同时线程2删除一个项目。线程1的结果可能包含对线程2删除的项的引用?可能还有其他问题。

我是否认为上述实现不是线程安全的?使用Scala使上述线程安全的最惯用方法是什么?

修改:如果可能,我宁愿避免阻止和同步。如果必须使用阻塞/同步,那么是否需要使用volatile参考?那不可变集合的重点是什么?难道我不能使用可变集合吗?

2 个答案:

答案 0 :(得分:3)

你正在使用写时复制方法,所以你的并发读写问题是它们没有严格的排序,但这不是一个真正的问题:它只是一个时间问题,如果A在写的同时B正在阅读,无法保证A是否会看到B的编辑。

你真正的问题是当你同时写C和D时:然后他们都可以读取相同的起始地图,更新自己的副本,然后只写自己的编辑。无论谁先写入都会覆盖他们的更改。

考虑包含(A,B)的起始地图,并且线程C和D分别添加条目“C”和“D”,而线程E和f读取地图;所有这一切同时发生。一个可能的重点是:

C读取地图(A,B)
D读取地图(A,B)
C写地图(A,B,C)
E读取地图(A,B,C)
D写地图(A,B,D)
F读取地图(A,B,D)

'C'条目显得很有效,然后永远丢失。

可靠地对写入进行排序的唯一方法是确保它永远不会同时输入。使用同步locke nforce单一条目写入块或通过使用单个Akka actor执行更新来确保序列化。

如果您关心读取和写入的顺序,则还需要同步读取,但如果您有多个线程访问它,那么这不太可能是真正的问题。

答案 1 :(得分:1)

  

我是否认为上述实现不是线程安全的?

是。它不是线程安全的。但它确实有正确的记忆visibility semantics

为简单起见,您可以通过以下方式使其成为线程安全:

class MyContainer[A <: {def uuid: UUID}] {

  def add(thing: A): Unit = this.synchronized{
    backingStore = backingStore + (thing.uuid -> thing)
  }

  def filter(p: A => Boolean): Option[Iterable[A]] = this.synchronized{
    val filteredThings = backingStore.values.filter(p)
    if (filteredThings.isEmpty) None else Some(filteredThings)
  }

  def remove(uuid: UUID): Option[A] = this.synchronized{
    backingStore.get(uuid) match {
      case optionalThing @ Some(thing) =>
        backingStore = backingStore - uuid; optionalThing
      case None => None
    }
  }

  import scala.collection.immutable.HashMap
  private[this] var backingStore = HashMap.empty[UUID, A]
}