Akka與Java內存模型的關係
不管你使用的Typesafe係統是Scala版本還是Java版本,都可以使你編寫並發程序的過程變得更加容易。這篇文章主要討論的是Typesafe係統,特別是針對Akka在並發程序中對共享內存的處理部分。
Java內存模型
在之前的Java 5 版本中,Java內存模型的定義是很值得商榷的。以至於在共享內存環境下的多線程處理的結果變得多種多樣,比如:
- 線程讀取不到其他線程寫入的值:內存可見性問題
- 線程得到了其他線程的“非正常”行為,這也是因為底層指令沒有按照期望的順序執行的結果:指令重排序問題
Java 5版本的JSR 133規範其實已經解決了很多這樣的問題。首先Java內存模型是以先序執行原則為前提的規則集合,也就是說它限製的結果是內存的訪問必須是依次進行,可是它同時又允許上述的行為亂序的執行。下麵是一些規則的例子:
- 監控鎖規則:在同一把鎖的獲取之前必須已經進行了鎖的釋放
- Volatile變量規則:對同一個volatile變量的讀取必須在對其寫入之後
盡管Java內存模型表麵上看很複雜,但是在規範的製定上一直在尋求高可用性以及使其具有可編寫高性能、擴展性良好的並發程序結構。
角色(Actor)和Java內存模型
針對Akka中的角色(Actor)實現,多線程可以有兩種方式來對共享內存進行操作:
- 如果消息被發送給一個角色(Actor)。在大多數情況下消息是不可變的,但是也有可能消息並不是如理想中的那樣完全不可變化,如果沒有先序執行規則的保證,接受消息的角色(Actor)有可能隻會看到初始化不完全的數據或者更嚴重的根本看不到。
- 如果角色(Actor)通過消息處理改變了內部狀態,然後處理其他的消息時又獲取內部狀態。在這種情況下你需要注意的是角色(Actor)模型並不能保證同一線程可以對不同的消息用相同的角色(Actor)處理。
為了避免角色(Actor)之間的可見性和重排序問題,Akka提供了下麵兩個先序執行的原則來保證:
- 角色(Actor)發送規則:發送消息給一個角色(Actor)的操作必須在同一個角色(Actor)接受了那條消息之前發生。
- 角色(Actor)後續處理規則:同一角色(Actor)的消息處理必須是串行的。
注意:通常意義下也就是當下一消息被角色(Actor)處理的時候,角色(Actor)內部字段的變化在當前是可見的。因此角色(Actor)中的字段不需要被聲明為volatile,也不需要是等效的。
上述兩個原則隻對同一角色(Actor)實例有效,如果使用的是不同的角色(Actor)則沒有任何作用。
Future和Java內存模型
注冊在Future上的所有回調必須在Future操作完成之後執行。
建議多使用final變量,如果使用了過多的非final變量也必須同時被標識為volatile以便可以在回調中使用字段的最新值。
如果使用過多引用類型,這種情況下必須保證引用指向的實例是線程安全的。我們強烈建議不要使用對象鎖,因為它可能造成性能低下,並且最嚴重的是可能導致死鎖的產生。這其實也是使用synchronized的危險所在。
軟件事務內存(STM)和Java內存模型
Akka的軟件事務內存也支持先序執行規則:
事務引用規則:針對同一事務引用的成功寫的提交操作必須發生在同一事務引用的後續讀操作之前。
這個規則看起來和Java內存模型中的volatile變量很相似。當前版本的Akka軟件事務模型支持延遲寫入,因此對於共享內存的實際的寫操作隻有在事務提交的時候才會執行。事務中的所有寫操作都會被置於本地緩衝並對其他事務不可見的,這就保證了不會發生髒讀現象。
在Akka中這些規則到底是怎樣的這都是實現細節而且都是一直在變動的,具體的下班取決於使用的配置。但是不管怎樣他們都是構建在比如監控鎖規則或者volatile變量規則這些Java虛擬機模型規則之上的。這就是說,如果你使用Akka,是不用添加同步來保證先序執行的關係的,因為這些Akka已經為你做了。你隻需要專注於業務邏輯即可,Akka框架會替你做好這些規則的保證。
角色(Actor)和共享可變狀態
但是畢竟Akka是運行在Java虛擬機上的,當然還是要遵守一些規則的。
使用內部角色(Actor)狀態並且將它暴露給其他線程
01 |
class MyActor extends Actor{
|
02 |
var state = ...
|
03 |
def receive = {
|
04 |
case _= >
|
05 |
//錯誤
|
06 |
//相當的糟糕,共享可變狀態
|
07 |
//會使你的應用出現奇怪的行為
|
08 |
Future { state = NewState }
|
09 |
anotherActor ? message onSuccess {r = > state = r }
|
10 |
11 |
//相當糟糕,“sender”會因為每條消息而改變
|
12 |
//共享可變狀態BUG
|
13 |
Future {expensiveCalculation(sender())}
|
14 |
15 |
//正確
|
16 |
//完全安全,“self”可以封裝
|
17 |
Future {expensiveCalculation() } onComplete { f = > self ! f.value.get }
|
18 |
19 |
//完全安全,我們封裝一個固定值
|
20 |
//而且它是一個ActorRef,這個對象是線程安全的
|
21 |
val currentSender = sender()
|
22 |
Future {expensiveCalculation(currentSender)}
|
23 |
}
|
24 |
} |
消息必須是不可變的,這樣可以避開共享可變狀態的陷阱。
最後更新:2017-05-23 16:33:23