Akka 与 Java 内存模型

优质
小牛编辑
128浏览
2023-12-01

使用包含Scala和Akka在内的Typesafe平台的主要好处是它简化了并发软件的编写过程。本文将讨论Typesafe平台,尤其是Akka是如何在并发应用中访问共享内存的。

Java内存模型

在Java 5之前,Java内存模型(JMM)定义是有问题的。当多个线程访问共享内存时很可能得到各种奇怪的结果,例如:

  • 一个线程看不到其它线程所写入的值:可见性问题
  • 由于指令没有按期望的顺序执行,一个线程观察到其它线程的 ‘不可能’ 行为:指令重排序问题

随着Java 5中JSR 133的实现,很多这种问题都被解决了。 JMM是一组基于 “发生在先” 关系的规则, 限制了一个内存访问行为何时必须在另一个内存访问行为之前发生,以及反过来,它们何时能够不按顺序发生。这些规则的两个例子包括:

  • 监视器锁规则: 对一个锁的释放先于所有后续对同一个锁的获取
  • volatile变量规则: 对一个volatile变量的写操作先于所有对同一volatile变量的后续读操作

虽然JMM看起来很复杂,但是其规范试图在易用性和编写高性能、可扩展的并发数据结构的能力之间寻找一个平衡。

Actors与Java内存模型

使用Akka中的Actor实现,有两种方法让多个线程对共享的内存进行操作:

  • 如果一条消息被(例如,从另一个actor)发送到一个actor,大多数情况下消息是不可变的,但是如果这条消息不是一个正确创建的不可变对象,如果没有 “发生先于” 规则, 有可能接收方会看到部分初始化的数据,甚至可能看到无中生有的数据(long/double)。
  • 如果一个actor在处理某条消息时改变了自己的内部状态,而之后又在处理其它消息时又访问了这个状态。一条很重要的需要了解的规则是,在使用actor模型时你无法保证,同一个线程会在处理不同的消息时使用同一个actor。

为了避免actor中的可见性和重排序问题,Akka保证以下两条 “发生在先” 规则:

  • actor发送规则 : 一条消息的发送动作先于目标actor对同一条消息的接收。
  • actor后续处理规则: 对同一个actor,一条消息的处理先于下一条消息处理

注意

通俗地说,这意味着当这个actor处理下一个消息的时候,对actor的内部字段的改变是可见的。因此,在你的actor中的域不需要是volitale或是同等可见性的。

这两条规则都只应用于同一个actor实例,对不同的actor则无效。

Future与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框架来保证这些规则的满足。

Actor与共享的可变状态

因为Akka运行在JVM上,所以还有一些其它的规则需要遵守。

  • 捕捉Actor内部状态并暴露给其它线程

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) }
  }
}
  • 消息应当是不可变的, 这是为了避开共享可变状态的陷阱。