使用包含Scala和Akka在内的 Typesafe 平台的主要好处是它简化了并发软件的编写过程。本文将讨论Typesafe平台,尤其是Akka是如何在并发应用中访问共享内存的。
在Java 5之前,Java内存模型(JMM)定义是有问题的。当多个线程访问共享内存时很可能得到各种奇怪的结果,例如:
随着Java 5中JSR 133的实现,很多这种问题都被解决了。 JMM是一组基于 “发生在先” 关系的规则, 限制了一个内存访问行为何时必须在另一个内存访问行为之前发生,以及反过来,它们何时能够不按顺序发生。这些规则的两个例子包括:
虽然JMM看起来很复杂,但是其规范试图在易用性和编写高性能、可扩展的并发数据结构的能力之间寻找一个平衡。
使用Akka中的Actor实现,有两种方法让多个线程对共享的内存进行操作:
为了避免actor中的可见性和重排序问题,Akka保证以下两条 “发生在先” 规则:
注意
通俗地说,这意味着当这个actor处理下一个消息的时候,对actor的内部字段的改变是可见的。因此,在你的actor中的域不需要是volitale或是同等可见性的。
这两条规则都只应用于同一个actor实例,对不同的actor则无效。
一个Future的完成 “先于” 任何注册到它的回调函数的执行。
我们建议不要在回调中捕捉(close over)非final的值 (Java中称final,Scala中称val), 如果你选择捕捉非final的字段,则它们必须被标记为volatile来让它的当前值对回调代码可见。
如果你捕捉一个引用,你还必须保证它所指代的实例是线程安全的。我们强烈建议远离使用锁的对象,因为它们会引入性能问题,甚至最坏可能造成死锁。这些是使用同步的风险。
Akka中的软件事务性内存 (STM) 也提供了一条 “发生在先” 规则:
这条规则非常象JMM中的“volatile 变量”规则。目前Akka STM只支持延迟写,所以对共享内存的实际写操作会被延迟到事务提交之时。在事务中发生的写操作会被存放在一个本地缓冲区内 (事务的写操作集) ,并且对其它事务是不可见的。这就是为什么脏读是不可能的。
这些规则在Akka中的实现会随时间而变化,精确的细节甚至可能依赖于所使用的配置。但是它们是建立在其它JMM规则之上的,如监视器锁规则、volatile变量规则。 这意味着Akka用户不需要操心为了提供“发生先于”关系而增加同步,因为这是Akka的工作。这样你可以腾出手来处理业务逻辑,让Akka框架来保证这些规则的满足。
因为Akka运行在JVM上,所以还有一些其它的规则需要遵守。
class MyActor extends Actor {
var state = ...
def receive = {
case _ =>
// 错误的做法
// 非常错误,共享可变状态,
// 会让应用莫名其妙地崩溃
Future { state = NewState }
anotherActor ? message onSuccess { r => state = r }
// 非常错误, 共享可变状态 bug
// "发送者"是一个可变变量,随每个消息改变
Future { expensiveCalculation(sender()) }
//正确的做法
// 非常安全, "self" 被闭包捕捉是安全的
// 并且它是一个Actor引用, 是线程安全的
Future { expensiveCalculation() } onComplete { f => self ! f.value.get }
// 非常安全,我们捕捉了一个固定值
// 并且它是一个Actor引用,是线程安全的
val currentSender = sender()
Future { expensiveCalculation(currentSender) }
}
}