來自:碼農翻身(微訊號:coderising)
張大胖在做一個銀行相關的專案,寫了一個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說:“是啊, 其實執行緒這個東西,就是一段程式碼的執行而已, 是作業系統層面的概念,可是我們苦逼的程式員不得不來面對它,來背這個多執行緒併發的鍋了。”
下班後,張大胖一直在思考這個問題:既然執行緒是作業系統層面的概念,能不能把執行緒的概念隱藏起來,然後所有的操作都不用加鎖呢? 這樣以來程式設計就會容易得多啊!
本質的問題是什麼?
首先是共享的狀態,例如Account中的balance ,多個執行緒都要讀寫, 其次就是多個執行緒亂序、併發執行。
能不能換個思路,把這個Account物件看成一個黑盒子,你想存款了,就發一個存款的訊息過來,想取款就發一個取款的訊息過來。
不管是有一個訊息,還是有100個訊息,我統統放到黑盒子的一個隊例中,然後讓Account物件一個個順序處理不就可以了? 根本不用在方法上加鎖!
這樣做,其實就是把併發的操作變成了序列的操作而已!
不對,如果呼叫方把取款訊息放下就走, 不等待傳回結果, 那就不是同步操作,而是非同步操作了!
但是如果取款的時候發現餘額不足,怎麼通知呼叫方?嗯,呼叫方也必須是個黑盒子物件,也向它傳送非同步訊息,這個訊息也會在訊息佇列中存下來,呼叫方“黑盒子”也會一個個處理。
想到這一層,張大胖激動起來:取款和存款的操作就不用在加鎖了,碼農們只要考慮黑盒子對訊息的處理即可:取出訊息,處理訊息,向別的黑盒子傳送訊息, 根本不用考慮執行緒這樣底層的概念了。
第二天張大胖趕緊找到Bill, 向他炫耀自己的“新發明”。
Bill不動聲色:“小夥子,不錯啊,重新發明瞭輪子!”
“重新發明?”
“是啊,你這個所謂黑盒子,就是所謂Actor模型啊! 它最早由Carl Hewitt在1973定義,其訊息傳遞的方式更加符合面向物件的原始意圖, 這一點我想你也體會到了,要不你怎麼把他們叫做黑盒子啊。”
“1973年? 我還沒出生。唉,看來這些概念已經被老前輩們都發明完了啊。”
“Actor屬於併發元件模型 ,可以把程式員從多執行緒併發或執行緒池等基礎概念中解放出來。它有這麼幾個特點:”
Actor:
就是你說的黑盒子,系統是由很多Actor組成。 Actor之間不共享狀態,但是會接收別的Actor傳送的非同步訊息,處理的過程中,會改變內部狀態,也可能向別的Actor傳送訊息。
Message:
訊息是不可變的, 它的傳送都是非同步的,Actor內部有個“MailBox”來快取訊息。
MailBox:
Actor內部快取訊息的郵箱, 其他Actor傳送的訊息都放到這裡,然後被本Actor處理,類似有多個生成者和一個消費者的隊例。
張大胖說:“和我之前的圖差不多,看來我確實是重新發明瞭輪子啊。”
Bill 笑道:“這個Actor看起來很美,但是程式設計的時候你得掃清一下你的思維才行。 大胖,之前你的轉賬操作在多執行緒下不是會出現死鎖嗎? 你考慮下,如果用Actor的思路該怎麼寫?”
“首先,得有兩個Actor, 這兩個Actor 表示了兩個賬戶,我把它們叫做旺財和小強。”
“然後呢,轉賬的邏輯怎麼處理?”
張大胖想了一會:“既然轉賬是在兩個Actor之間發生的,那可以引入一個協調者Actor,叫做轉賬管家吧。不過,由於訊息都是非同步的,轉賬管家向旺財這個Actor發起扣款請求以後,不知道什麼時候才能真正執行扣款,也不能立刻知道是否成功,必須得等待啊,這就有點麻煩了。”
Bill說:“我給你畫個流程圖,你看看。”
張大胖感慨地說:“原來的多執行緒併發模型,需要同時鎖住兩個賬戶,然後才能進行轉賬。現在每個Actor都獨立,也把這個轉賬給搞定了。”
Bill說:“其實對於轉賬管家來說,對每個轉賬的訊息,內部是隱含一個流程狀態的,就是先向某個賬戶扣款,成功以後再向另一個賬戶增加,最後給呼叫者傳回狀態,這個次序是不能亂的。看到圖中那個Transaction ID沒有(Tx01),就是用來跟蹤這個轉賬的事務。”
“我發現了一個漏洞,你這個轉賬雖然看起來很美,沒有加鎖,但是和原來的是有區別的,原來多執行緒思路是會把旺財和小強的賬戶同時鎖住,然後轉賬,在這個過程中,別人是不能操作這兩個賬號的! 而你的Actor方案中,當轉賬管家給旺財發訊息扣款的時候,小強其實是自由的,如果這時候小強的賬戶被凍結,那你的轉賬管家還得回滾旺財的扣款,這多麻煩啊。”
Bill:“哈哈,你小子還挺機靈的嘛,看出了這個問題,Actor模型非常適用於多個元件獨立工作,相互之間僅僅依靠訊息傳遞的情況。如果想在多個元件之間維持一致的狀態(比如咱們例子中的轉賬),那就不爽了。”
“那怎麼解決這個問題?”
“那必須得用一些特殊手段了,有些實現Actor的框架,例如Akka,專門提供了像Coordinated /Transactor這樣的機制來處理這個問題。有空的話給你仔細講講。”
“好吧,我回頭看看這個Akka, 對了, Actor雖然對使用者隱藏了執行緒, 但是總得有執行緒來處理訊息吧。” 張大胖問道。
“那是肯定的,執行緒本質上就是一段程式碼的執行,每個Actor在處理訊息的時候,肯定得和執行緒關聯才行,只不過Actor系統把執行緒這個概念給隱藏了。”
“有哪些系統實現了Actor?” 張大胖接著問。
“其實最著名的就是Erlang了,Actor模型可以說是它的基礎,除了我們上面所說的,還可以讓Actor之間建立關聯,例如讓一個Actor去監控另外一些Actor工作,如果那些Actor崩潰了,就新建一個Actor繼續工作。在Java 領域,剛才提到的Akka是比較知名的一個Actor框架。 ”
(完)
●本文編號598,以後想閱讀這篇文章直接輸入598即可
●輸入m獲取文章目錄
Web開發
更多推薦《18個技術類微信公眾號》
涵蓋:程式人生、演演算法與資料結構、駭客技術與網路安全、大資料技術、前端開發、Java、Python、Web開發、安卓開發、iOS開發、C/C++、.NET、Linux、資料庫、運維等。