我是Scala的新手。
我正在尝试弄清楚如何确保Scala对象(也称为单例)中的函数线程安全。
根据我目前所了解的,似乎应该将可见性限制在函数作用域(或以下),并尽可能使用不可变变量。然而,我还没有看到违反线程安全的示例,因此我不确定还需要采取哪些其他预防措施。
有人能否指向一篇关于这个问题的好文章,最好附带违反线程安全的示例?
我是Scala的新手。
我正在尝试弄清楚如何确保Scala对象(也称为单例)中的函数线程安全。
根据我目前所了解的,似乎应该将可见性限制在函数作用域(或以下),并尽可能使用不可变变量。然而,我还没有看到违反线程安全的示例,因此我不确定还需要采取哪些其他预防措施。
有人能否指向一篇关于这个问题的好文章,最好附带违反线程安全的示例?
object WritesExample {
var myList: List[Int] = List.empty
}
想象一下,我们有两个线程同时访问WritesExample
,每个线程都执行以下updateList
def updateList(x: WritesExample.type): Unit =
WritesExample.myList = 1 :: WritesExample.myList
WritesExample.myList
的length
为2。但是,如果两个线程在另一个线程完成写入之前都读取了WritesExample.myList
,那么情况可能并非如此。如果两个线程读取WritesExample.myList
时它为空,那么两个线程都会写回长度为1的列表,其中一个写操作会覆盖另一个,因此最终WritesExample.myList
的长度仅为1。因此,我们实际上失去了应该执行的写操作。这不好。
现在让我们看一下不准确的读取。
object ReadsExample {
val myMutableList: collection.mutable.MutableList[Int]
}
ReadsExample
。这一次,它们中的每一个都会重复执行updateList2
。def updateList2(x: ReadsExample.type): Unit =
ReadsExample.myMutableList += ReadsExample.myMutableList.length
updateList2
时,仅生成递增数字的有序列表,例如0、1、2、3、4...
。不幸的是,在多个线程同时使用updateList2
访问ReadsExample.myMutableList
时,可能会出现在读取ReadsExample.myMutableList.length
和最终写入之间,ReadsExample.myMutableList
已经被另一个线程修改的情况。因此,理论上您可能会看到像0、0、1、1
这样的东西,或者如果一个线程需要比另一个线程更长时间才能写入,则可能会看到0、1、2、1
(其中较慢的线程在另一个线程已经访问并写入列表三次后最终写入列表)。ReadsList
示例以使用不可变的List
(和一个var
),那么我可以自信地说列表的整数元素始终对应于列表的索引。这意味着您的程序很少会进入不一致的状态(例如,可以想象当并发变异时,天真的可变集合实现可能会出现非唯一元素)。而且事实证明,处理并发的现代技术通常非常擅长处理缺失写入。锁(也称直接尝试序列化读/写对):这通常是你首先听到的处理并发的基本方法。想要访问状态的每个进程都会首先在其上放置一个锁。现在任何其他进程都被排除在访问该状态之外。然后该进程写入该状态,并在完成时释放锁。其他进程现在可以重复此过程。在我们的WritesExample
中,updateList
在执行和释放锁之前将首先获取锁;这将防止其他进程读取WritesExample.myList
,直到写入完成,从而防止它们看到可能导致破坏性写入的旧版本myList
(请注意,还有更复杂的锁定过程,允许同时读取,但让我们现在坚持基础知识)。
锁通常不适用于多个状态。使用多个锁时,通常需要按特定顺序获取和释放锁,否则您可能会死锁或活锁。
Oracle和Twitter文档提供了对此方法的良好概述。
描述您的操作,而不是执行它(即建立一个连续表示您的操作的序列,并让其他人处理它):不直接访问和修改状态,而是描述如何执行此操作,然后将其交给其他人来实际执行。例如,您可以向对象传递消息(例如Scala中的actors),该对象会将这些请求排队,然后逐个在其从未直接公开给任何其他人的内部状态上执行它们。在actors的特定情况下,通过消除显式获取和释放锁的需求,改善了锁的情况。只要您将需要同时访问的所有状态封装在单个对象中,消息传递就非常有效。当您在多个对象之间分布状态时,actors会崩溃(因此,在此范例中,强烈不建议这样做)。 Akka actors是Scala中的一个很好的例子。事务(也称为暂时将某些读写操作与其他操作隔离,并让隔离系统为您序列化):将所有读/写操作都包装在事务中,以确保在进行读/写操作期间,您对世界的看法与任何其他更改隔离。通常有两种实现方式。一种是类似锁定的方法,在事务运行时防止其他人访问数据;另一种是在检测到共享状态发生更改时从头开始重新启动事务并放弃任何进度(通常是为了提高性能)。一方面,与锁和演员不同,事务很好地适用于不同的状态片段。只需将所有访问操作都包装在事务中即可。另一方面,您的读/写操作必须无副作用,因为它们可能会被多次抛弃和重试,并且您无法真正撤消大多数副作用。
如果你真的很不幸,虽然使用良好的事务实现通常不会真正死锁,但长时间运行的事务可能会不断被其他短暂事务打断,导致它不断被抛弃和重试,从而永远无法成功(这相当于活锁)。实际上,您正在放弃对序列化顺序的直接控制,并希望您的事务系统合理地排序。
Scala的STM库是此方法的一个很好的示例。
消除共享状态:最终的“解决方案”是重新思考问题,尝试思考您是否真正需要可写的全局共享状态。如果您不需要可写的共享状态,则并发问题将完全消失!
生活中的一切都是关于权衡的,而并发也不例外。在考虑并发时,首先要了解你拥有什么状态以及你想要保留哪些不变量。然后使用这些来指导你决定使用什么样的工具来解决问题。
Thread Safety Problem
部分可能会引起您的兴趣。简而言之,它使用一个简单的例子说明了线程安全问题,并概述了解决该问题的3种不同方法,即synchronization
、volatile
和AtomicReference
。synchronized
点、访问volatile
引用或取消引用AtomicReferences
时,Java会强制刷新其缓存行并提供数据的一致视图。AtomicReference
是这两个选择中最昂贵的。volatile
和synchronized
建立在Java的内置监视器之上。如果没有争用,监视器的成本非常低。由于synchronized
允许您更细粒度地控制何时同步,因此争用会更少,所以synchronized
往往是最便宜的选项。如果您的对象包含可以在并发情况下进行修改的状态,则无论是使用Scala还是其他编程语言,都可能会破坏线程安全性,这取决于实现方式。例如:
object BankAccount {
private var balance: Long = 0L
def deposit(amount: Long): Unit = balance += amount
}
object BankAccount {
private var balance: Long = 0L
def deposit(amount: Long): Unit =
this.synchronized {
balance += amount
}
}