Akka 指南 之「Akka 和 Java 內存模型」

溫馨提示:Akka 中文指南的 GitHub 地址爲「akka-guide」,歡迎大家StarFork,糾錯。

Akka 和 Java 內存模型

使用 LightBend 平臺(包括 Scala 和 Akka)的一個主要好處是簡化了併發軟件的編寫過程。本文討論了 LightBend 平臺,特別是 Akka 如何在併發應用程序中處理共享內存。

Java 內存模型

在 Java 5 之前,Java 內存模型(JMM)是定義有問題的。當多個線程訪問共享內存時,可能會得到各種奇怪的結果,例如:

  • 線程看不到其他線程寫入的值:可見性問題。
  • 由於沒有按預期的順序執行指令而導致的觀察其他線程發生“不可能”的行爲:指令重新排序問題。

隨着 Java 5 中 JSR 133 的實現,許多問題得到了解決。JMM 是基於“先於發生(happens-before)”關係的一組規則,它約束一個內存訪問必須發生在另一個內存訪問之前的時間,反之,當它們被允許無序發生時。這些規則的兩個例子是:

  • 監視器鎖規則:在每次後續獲取同一鎖之前,都會釋放一個鎖。
  • volatile變量規則volatile變量的寫入發生在同一volatile變量的每次後續讀取之前。

雖然 JMM 看起來很複雜,但是規範試圖在易用性和編寫性能、可擴展併發數據結構的能力之間找到一個平衡點。

Actors 和 Java 內存模型

通過 Akka 中的 Actor 實現,多個線程可以通過兩種方式在共享內存上執行操作:

  • 如果消息發送給某個 Actor(例如由另一個 Actor)。在大多數情況下,消息是不可變的,但是如果該消息不是正確構造的不可變對象,沒有“先於發生”規則,則接收者可能會看到部分初始化的數據結構,甚至可能會看到空氣稀薄的值(longs/doubles)。
  • 如果 Actor 在處理消息時更改其內部狀態,並在稍後處理另一條消息時訪問該狀態。重要的是要認識到,對於 Actor 模型,你不能保證同一線程將對不同的消息執行相同的 Actor。

爲了防止 Actor 出現可見性和重新排序問題,Akka 保證以下兩條“先於發生”規則:

  • Actor 發送規則:向 Actor 發送消息的過程發生在同一 Actor 接收消息之前。
  • Actor 後續處理規則:一條消息的處理髮生在同一 Actor 處理下一條消息之前。

註釋:在外行術語中,這意味着當 Actor 處理下一條消息時,Actor 內部字段的更改是可見的。因此,Actor 中的字段不必是volatileequivalent的。

這兩個規則僅適用於同一個 Actor 實例,如果使用不同的 Actor,則這兩個規則無效。

Futures 和 Java 存儲模型

Future的“先於發生”調用任何註冊到它的回調被執行之前。

我們建議不要關閉非final字段(Java 中的final和 Scala 中的val),如果選擇關閉非final字段,則必須標記volatile,以便字段的當前值對回調可見。

如果關閉引用,還必須確保引用的實例是線程安全的。我們強烈建議遠離使用鎖定的對象,因爲它可能會導致性能問題,在最壞的情況下還會導致死鎖。這就是同步的危險。

Actors 和共享可變狀態

由於 Akka 在 JVM 上運行,所以仍然需要遵循一些規則。

  • 關閉內部 Actor 狀態並將其暴露給其他線程
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 = {
    // this is a very costly operation
    "Meaning of life is 42"
  }

  def expensiveCalculation(): String = {
    // this is a very costly operation
    "Meaning of life is 42"
  }

  def receive = {
    case _ ⇒
      implicit val ec = context.dispatcher
      implicit val timeout = Timeout(5 seconds) // needed for `?` below

      // Example of incorrect approach
      // Very bad: shared mutable state will cause your
      // application to break in weird ways
      Future { state = "This will race" }
      ((echoActor ? Message("With this other one")).mapTo[Message])
        .foreach { received ⇒ state = received.msg }

      // Very bad: shared mutable object allows
      // the other actor to mutate your own state,
      // or worse, you might get weird race conditions
      cleanUpActor ! mySet

      // Very bad: "sender" changes for every message,
      // shared mutable state bug
      Future { expensiveCalculation(sender()) }

      // Example of correct approach
      // Completely safe: "self" is OK to close over
      // and it's an ActorRef, which is thread-safe
      Future { expensiveCalculation() } foreach { self ! _ }

      // Completely safe: we close over a fixed value
      // and it's an ActorRef, which is thread-safe
      val currentSender = sender()
      Future { expensiveCalculation(currentSender) }
  }
}
  • 消息應該是不可變的,這是爲了避免共享可變狀態陷阱。

英文原文鏈接Akka and the Java Memory Model.


———— ☆☆☆ —— 返回 -> Akka 中文指南 <- 目錄 —— ☆☆☆ ————

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章