Akka学习笔记(五):Akka与Java的内存模型
Akka简化了编写并发软件的过程,本文主要讨论Akka如何在并发应用中访问共享内存。
Java内存模型
Java5之前的JMM是相当混乱的。多线程访问共享内存很有可能会得奇怪的结果,如:
- 可见性问题,无法及时看到其他线程写入的值
- 指令乱序,观测到其他线程不可能的行为
从Java 5的JSR 133的实现,很多问题就解决了。JMM是基于一组"happens-before"关联规则,限制了访问内存的行为必须在另一个内存访问行为之前发生。当不想按顺序发生时,可以使用:
- 监视器锁规则:对一个锁的释放先于所有后续对同一个锁的获取
- volatile变量规则:对一个volatile变量的写操作先于所有后续对同一个volatile变量的读
JMM看起来很复杂,但是其规范试图在编写高性能,并发数据结构的能力间寻找平衡
Actor与Java内存模型
使用Akka的Actor,有两种方法可以使多线程操作共享内存:
- 假如一个message被发送给一个actor,在大多数情况下,message是不可变对象,万一message不是不可变的,没有”happen before"规则,receiver可能会看到部分初始化的数据,甚至可能看到无中生有的数据(long/double)
- 如果一个actor在处理消息时,改变了自己的内部状态,而后又在处理其他消息的时候访问了这个状态。我们需要知道的是,在使用Actor模型时,无法保证同一个线程在处理不同消息时,使用同一个actor(是指一个线程中有多个actor?还是一个actor改变了自己内部的状态后,就不是同一个actor?)
为了防止actor出现可见性问题,执行顺序问题,Akka制定了如下"happen before"规则:
- 发送规则:一条消息的发送动作先于同一个actor对同一条消息的接收
- actor后续处理规则:一条消息的处理,优先于同一个actor的下一条消息
两条规则只对同一个actor实例有效
通俗的解释:actor的内部变量(internal fields)是可见的,当下一个消息准备被处理时。所以你的actor的内部变量必须是volatile或者equivalent。
Futures与Java内存模型
一个Future的完成 “先于” 任何注册到它的回调函数的执行。
我们建议不要捕捉(close over)非final的值 (Java中称final,Scala中称val), 如果你 一定 要捕捉非final的值, 它们必须被标记为 volatile 来让它的当前值对回调代码可见。
如果你捕捉一个引用,, 你还必须保证它所指代的实例是线程安全的。 我们强烈建议远离使用锁的对象,因为它们会引入性能问题,甚至可能造成死锁。 这些是使用synchronized的风险。
STM与Java内存模型
Akka中的软件事务性内存 (STM) 也提供了一条 “发生在先” 规则:
- 事务性引用规则: 在提交过程中对一个事务性引用的成功的写操作先于所有对同一事务性引用的后续读操作发生。
这条规则非常象JMM中的 ‘volatile 变量’ 规则. 目前Akka STM只支持延迟写,所以对共享内存的实际写操作会被延迟到事务提交之时。事务中的写操作被存放在一个本地缓冲区中 (事务的写操作集) ,对其它的事务是不可见的。这就是为什么脏读是不可能的。
这些规则在Akka中的实现会随时间而变化,精确的细节甚至可能依赖于所使用的配置。但是它们是建立在其它的JMM规则如监视器锁规则和volatile变量规则基础上的。 这意味着Akka用户不需要操心为了提供“发生先于”关系而增加同步,因为这是Akka的工作。这样你可以腾出手来处理你的业务逻辑,让Akka框架来保证这些规则的满足。
Actors与共享的可变状态
由于Akka运行在JVM,有些规则仍然必须遵守
-
捕捉actor内部状态并暴露给其他线程
class MyActor extends Actor { var state = ... def receive = { case _ => //错误示范 // Very bad, 共享可变状态, // will break your application in weird ways Future { state = NewState } anotherActor ? message onSuccess { r => state = r } // Very bad, "sender" 随每个消息改变, //共享可变状态 bug Future { expensiveCalculation(sender()) } //正确示范 // Completely safe, "self" is OK to close over // and it's an ActorRef, which is thread-safe Future { expensiveCalculation() } onComplete { f => self ! f.value.get } // 非常安全,我们捕捉了一个固定值 // 并且它是一个Actor引用,是线程安全的 val currentSender = sender() Future { expensiveCalculation(currentSender) } } }
- 消息应该是不可变的,为了避免共享可变状态的陷阱