多線程併發的難題
張大胖在做一個銀行相關的項目,寫了一個Account的類,用來表示一個用戶的銀行賬號,根據銀行的常規業務,自然要提供兩個方法,存款(deposit)和取款(withdraw)。
爲了防止多線程併發時導致的數據不一致問題,張大胖給每個方法都加了synchronized, 那意思很清楚,想進入某個方法執行存款或取款操作,必須得先獲得一把鎖纔行。
(注:爲了簡化,這裏沒有做邊界條件檢查。)
但是在做轉賬操作的時候,爲了保證一致性,必須得把兩個賬戶都加上鎖,然後纔可以操作,於是張大胖寫下了這樣的代碼,他覺得很簡單,立刻就提交給Bill ,讓他Review。
富有經驗的Bill立刻就發現了問題,馬上對張大胖說:“這樣會出現死鎖!”
張大胖說:“這麼簡單的代碼,怎麼可能有死鎖?”
“假設線程1 做的操作是賬戶A給賬戶B轉賬, 先鎖住了A賬戶, 接下來試圖申請B賬戶的鎖;
與此同時線程2 在從 賬戶B給賬戶A 轉賬, 先鎖住了B賬戶的鎖, 接下來試圖申請A賬戶的鎖。
兩個線程各自持有資源, 然後等待獲取對方的資源, 都無法執行下去, 死鎖就出現了!”
張大胖無言以對,不得不承認Bill是正確的。他問道:“那怎麼解決這個問題?”
“非常簡單,加鎖的時候按次序來就可以了,例如所有的線程,無論是從A向B轉賬,還是從B向A轉賬,都先獲得賬號A的鎖,成功後再獲得賬戶B的鎖,這樣就沒問題了。”
張大胖說:“那樣代碼會變得很古怪啊,還得給兩個賬戶排個順序,如果不知道背後的思想讀起來很痛苦,怪不得人家說多線程編程很難啊。”
Bill說:“是啊, 其實線程這個東西,就是一段代碼的執行而已, 是操作系統層面的概念,可是我們苦逼的程序員不得不來面對它,來背這個多線程併發的鍋了。”
2
黑盒子
下班後,張大胖一直在思考這個問題:既然線程是操作系統層面的概念,能不能把線程的概念隱藏起來,然後所有的操作都不用加鎖呢? 這樣以來編程就會容易得多啊!
本質的問題是什麼?
首先是共享的狀態,例如Account中的balance ,多個線程都要讀寫, 其次就是多個線程亂序、併發執行。
能不能換個思路,把這個Account對象看成一個黑盒子,你想存款了,就發一個存款的消息過來,想取款就發一個取款的消息過來。
不管是有一個消息,還是有100個消息,我統統放到黑盒子的一個隊例中,然後讓Account對象一個個順序處理不就可以了? 根本不用在方法上加鎖!
這樣做,其實就是把併發的操作變成了串行的操作而已!
不對,如果調用方把取款消息放下就走, 不等待返回結果, 那就不是同步操作,而是異步操作了!
但是如果取款的時候發現餘額不足,怎麼通知調用方?嗯,調用方也必須是個黑盒子對象,也向它發送異步消息,這個消息也會在消息隊列中存下來,調用方“黑盒子”也會一個個處理。
想到這一層,張大胖激動起來:取款和存款的操作就不用在加鎖了,碼農們只要考慮黑盒子對消息的處理即可:取出消息,處理消息,向別的黑盒子發送消息, 根本不用考慮線程這樣底層的概念了。
3
Actor模型
第二天張大胖趕緊找到Bill, 向他炫耀自己的“新發明”。
Bill不動聲色:“小夥子,不錯啊,重新發明了輪子!”
“重新發明?”
“是啊,你這個所謂黑盒子,就是所謂Actor模型啊! 它最早由Carl Hewitt在1973定義,其消息傳遞的方式更加符合面向對象的原始意圖, 這一點我想你也體會到了,要不你怎麼把他們叫做黑盒子啊。”
“1973年? 我還沒出生。唉,看來這些概念已經被老前輩們都發明完了啊。”
“Actor屬於併發組件模型 ,可以把程序員從多線程併發或線程池等基礎概念中解放出來。它有這麼幾個特點:”
Actor:
就是你說的黑盒子,系統是由很多Actor組成。 Actor之間不共享狀態,但是會接收別的Actor發送的異步消息,處理的過程中,會改變內部狀態,也可能向別的Actor發送消息。
Message:
消息是不可變的, 它的發送都是異步的,Actor內部有個“MailBox”來緩存消息。
MailBox:
Actor內部緩存消息的郵箱, 其他Actor發送的消息都放到這裏,然後被本Actor處理,類似有多個生成者和一個消費者的隊例。
張大胖說:“和我之前的圖差不多,看來我確實是重新發明了輪子啊。”
4
用Actor實現轉賬
Bill 笑道:“這個Actor看起來很美,但是編程的時候你得刷新一下你的思維纔行。 大胖,之前你的轉賬操作在多線程下不是會出現死鎖嗎? 你考慮下,如果用Actor的思路該怎麼寫?”
“首先,得有兩個Actor, 這兩個Actor 表示了兩個賬戶,我把它們叫做旺財和小強。”
“然後呢,轉賬的邏輯怎麼處理?”
張大胖想了一會:“既然轉賬是在兩個Actor之間發生的,那可以引入一個協調者Actor,叫做轉賬管家吧。不過,由於消息都是異步的,轉賬管家向旺財這個Actor發起扣款請求以後,不知道什麼時候才能真正執行扣款,也不能立刻知道是否成功,必須得等待啊,這就有點麻煩了。”
Bill說:“我給你畫個流程圖,你看看。”
張大胖感慨地說:“原來的多線程併發模型,需要同時鎖住兩個賬戶,然後才能進行轉賬。現在每個Actor都獨立,也把這個轉賬給搞定了。”
Bill說:“其實對於轉賬管家來說,對每個轉賬的消息,內部是隱含一個流程狀態的,就是先向某個賬戶扣款,成功以後再向另一個賬戶增加,最後給調用者返回狀態,這個次序是不能亂的。看到圖中那個Transaction ID沒有(Tx01),就是用來跟蹤這個轉賬的事務。”
4
漏洞
“我發現了一個漏洞,你這個轉賬雖然看起來很美,沒有加鎖,但是和原來的是有區別的,原來多線程思路是會把旺財和小強的賬戶同時鎖住,然後轉賬,在這個過程中,別人是不能操作這兩個賬號的! 而你的Actor方案中,當轉賬管家給旺財發消息扣款的時候,小強其實是自由的,如果這時候小強的賬戶被凍結,那你的轉賬管家還得回滾旺財的扣款,這多麻煩啊。”
Bill:“哈哈,你小子還挺機靈的嘛,看出了這個問題,Actor模型非常適用於多個組件獨立工作,相互之間僅僅依靠消息傳遞的情況。如果想在多個組件之間維持一致的狀態(比如咱們例子中的轉賬),那就不爽了。”
“那怎麼解決這個問題?”
“那必須得用一些特殊手段了,有些實現Actor的框架,例如Akka,專門提供了像Coordinated /Transactor這樣的機制來處理這個問題。有空的話給你仔細講講。”
“好吧,我回頭看看這個Akka, 對了, Actor雖然對用戶隱藏了線程, 但是總得有線程來處理消息吧。” 張大胖問道。
“那是肯定的,線程本質上就是一段代碼的執行,每個Actor在處理消息的時候,肯定得和線程關聯纔行,只不過Actor系統把線程這個概念給隱藏了。”
“有哪些系統實現了Actor?” 張大胖接着問。
“其實最著名的就是Erlang了,Actor模型可以說是它的基礎,除了我們上面所說的,還可以讓Actor之間建立關聯,例如讓一個Actor去監控另外一些Actor工作,如果那些Actor崩潰了,就新建一個Actor繼續工作。在Java 領域,剛纔提到的Akka是比較知名的一個Actor框架。 ”