目录
使用Lightbend平台(包括Scala和Akka)的一个主要好处是它简化了编写并发软件的过程,本文讨论Lightbend平台,尤其是Akka在并发应用程序中如何处理共享内存的。
Java内存模型
在Java5之前,Java内存模型的定义不明确。 当多个线程访问共享内存时,有可能获得各种奇怪的结果,例如:
- 一个线程没有看到其他线程写的值:一个可见性问题
- 一个线程观察其他线程的“不可能”行为,这是由于指令未按预期顺序执行引起的:指令重排序问题
随着Java 5中JSR 133的实现,这些问题很多都得到了解决。 JMM是一组基于“happens-before”关系的规则,这限制了一个内存访问的操作必须在另一个内存访问操作之前发生,反过来,也阐明了何时允许不按顺序访问。两个规则的示例如下:
- 监视器锁定规则:一个锁的释放happens before后面对同一个锁的获取
- volatile变量规则:volatile变量的写happens before后面对相同volatile变量的读
尽管JMM看起来很复杂,但规范试图在易用性和编写高性能和可伸缩并发数据结构之间找到平衡点。
Actors和Java内存模型
在Akka的Actors实现中,多个线程可以通过两种方式在共享内存上执行操作:
- 如果消息被发送给一个Actor(例如由另一个Actor)。 在大多数情况下,消息是不可变的,但如果该消息是一个没有被正确构造的不可变对象,那么没有“happens before”规则,接收的Actor就有可能看到部分初始化的数据结构,甚至可能是毫无意义的数据(long和double,64字节,高低位问题)
- 如果一个Actor在处理消息时改变了其内部状态,并在稍后处理另一个消息时访问该状态。需要注意的是,使用Actor模型,无法保证同一个线程会执行相同Actor的不同消息。
为了防止Actor的可见性和重排序问题,Akka保证以下两个“happens before”规则:
- Actor发送规则:一个Actor的发送消息 happens before 同一个Actor的接收消息
- Actor后续处理规则:在同一Actor中的一个消息处理 happens before 下一个消息的处理
Note
在外行人的术语中,这意味着Actor内部字段的改变对于下一条要处理的消息是可见的。 因此,Actor中的字段不必是volatile或equivalent。
这两个规则仅适用于同一个Actor实例,对于不同的Actor,则无效。
Futures和Java内存模型
一个Future的完成happens before注册到它上面的任何回调函数的执行。
我们建议不要倾向于使用非final字段(Java中的final和Scala中的val),如果确实需要使用非final字段,则必须将它们标记为volatile,使得该字段的当前值对回调函数可见。
如果你使用引用,则还必须确保引用的实例是线程安全的。 我们强烈建议远离使用锁的对象,因为它们可能会引入性能问题,并且在最坏的情况下会导致死锁。 这是使用sychronized的危险。
Actors和共享可变状态
由于Akka在JVM上运行,因此仍然需要遵循一些规则。
- 使用内部状态并暴露给其他线程
import akka.actor.{Actor, ActorRef}
import akka.pattern.ask
import akka.util.Timeout
import scala.concurrent.Future
import scala.concurrent.duration._
import scala.language.postfixOps
import scala.collection.mutable
case class Message(msg: String)
class EchoActor extends Actor {
def receive = {
case msg => sender() ! msg
}
}
class CleanUpActor extends Actor {
def receive = {
case set: mutable.Set[_] => set.clear()
}
}
class MyActor(echoActor: ActorRef, cleanUpActor: ActorRef) extends Actor {
var state = ""
val mySet = mutable.Set[String]()
def expensiveCalculation(actorRef: ActorRef): String = {
// 这里省略了一个非常耗时的操作
"Meaning of life is 42"
}
def expensiveCalculation(): String = {
// 这里省略了一个非常耗时的操作
"Meaning of life is 42"
}
def receive = {
case _ =>
implicit val ec = context.dispatcher
implicit val timeout = Timeout(5 seconds) // ask模式需要使用
// 不正确方式的示例
// 在future及ask返回消息处理中共享可变对象
Future {
state = "This will race"
}
(echoActor ? Message("With this other one")).mapTo[Message]
.foreach { received => state = received.msg }
// 非常差: 共享可变对象允许其他Actor更改它的状态
// 而且可能存在奇怪的竞态条件
cleanUpActor ! mySet
// 非常差: "sender"对于每个消息可能不一样
Future {
expensiveCalculation(sender())
}
// 示例正确方式,"self"可以使用,因为是一个线程安全的ActorRef
Future {
expensiveCalculation()
} foreach {
self ! _
}
// 完全安全: 我们使用了一个定值,而且是线程安全的ActorRef
val currentSender = sender()
Future {
expensiveCalculation(currentSender)
}
}
}
- 消息应该是不可变的,这是为了避免共享可变状态的陷阱