我想创建一个使用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参考?那不可变集合的重点是什么?难道我不能使用可变集合吗?
答案 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]
}