旺財和小強的三生三世
***世
旺財和小強是線程池的兩個線程, 他們經(jīng)常做的工作就是對一個數(shù)加加減減,用人類的話來說就是存款,取款。
- public class Account{
- private int balance;
- public synchronized void deposit(int amt){
- balance += amt;
- }
- public synchronized void withdraw(int amt){
- if(balance >= amt){
- balance -= amt;
- }
- throw new RuntimeException("insufficent blance");
- }
- }
(友情提示,可左右滑動,下同)
每次進行存款,取款操作的時候,他們兩個都需要獲得一把鎖,這樣就能保證同一時刻只有一個人在修改,不會出亂子。
這一天,他們倆又遇到了一個叫做轉(zhuǎn)賬的操作:
- public void transfer(Account from,Account to, int amt){
- synchronized(from){
- synchronized(to){
- from.withdraw(amt);
- to.deposit(amt);
- }
- }
- }
旺財說:“這個程序員不錯,考慮得挺周全。轉(zhuǎn)賬的時候把兩個賬戶都鎖住了,安全!”
小強說:“沒錯,執(zhí)行吧。”
旺財這個線程從A向B轉(zhuǎn)賬 , 與此同時,小強從B向A轉(zhuǎn)賬
令旺財和小強沒有想到的是,居然出現(xiàn)了死鎖。
類似的事件發(fā)生不少, 線程池的線程用光了,Tomcat被迫重啟,這個世界毀滅了。
第二世
新生代的旺財和小強從線程池中出來, Tomcat老大給他們講了上一代旺財和小強的故事, 對他們諄諄教導:“做轉(zhuǎn)賬操作的時候一定要小心,別死鎖了!”
旺財和小強有點兒憤憤不平:“這我們倆也控制不了啊,這要看程序員寫的代碼,以及操作系統(tǒng)中的線程調(diào)度啊!”
不滿歸不滿,他倆還是有點小期待,想看看可怕的轉(zhuǎn)賬代碼到底怎么樣。
沒過多久, 他倆就如愿了:
- public static final Object lock = new Object();
- public void transfer(Account from,Account to, int amt){
- int fromHash = System.identityHashCode(from);
- int toHash = System.identityHashCode(to);
- if(fromHash > toHash){
- synchronized(from){
- synchronized(to){
- from.withdraw(amt);
- to.deposit(amt);
- }
- }
- }
- else if(toHash > fromHash){
- synchronized(to){
- synchronized(from){
- from.withdraw(amt);
- to.deposit(amt);
- }
- }
- }
- else {
- synchronized(lock){
- synchronized(from){
- synchronized(to){
- from.withdraw(amt);
- to.deposit(amt);
- }
- }
- }
- }
- }
看到這樣的代碼, 旺財?shù)刮艘豢跉?,撓著頭說:“搞什么鬼,轉(zhuǎn)個賬都這么麻煩!”
小強說:“老大不是說了嗎,上一代線程老是在轉(zhuǎn)賬這里出錯,于是代碼就重寫了。你看,這一次寫得就很嚴謹了,每一次都會去比較兩個賬戶的大小(通過hash code),誰大就先獲得誰的鎖。 ”
旺財說:“奧,相當于把賬戶給排了序,假設賬戶A大于賬戶B , 那我們倆轉(zhuǎn)賬的時候,每次都先獲得A的鎖,這樣就不會互相等待了。 ”
“沒錯,還有一個特殊情況,如果這兩個賬戶的hash code 相同,那就再去競爭另外一個特殊的鎖,誰搶到誰就可以先執(zhí)行。另一個就在那里等待。”
旺財和小強這次順利地把轉(zhuǎn)賬給執(zhí)行完了,回去給Tomcat匯報了一遍。
Tomcat老大感慨地說:“有這么復雜的代碼,可見使用‘共享內(nèi)存’的方式來并發(fā)編程很不容易啊!”
“共享內(nèi)存?”
“對啊,你看這些賬戶的數(shù)據(jù),每個線程都可以訪問,不就是共享內(nèi)存嗎, 為了能夠安全訪問,只有來‘加鎖’了。 古人說,這個世界上有兩種構建軟件的方式,一種方法是使其足夠簡單以至于不存在明顯的缺陷,另外一種是使其足夠復雜以至于看不出有什么問題。我很擔心啊, 現(xiàn)在這個系統(tǒng)就屬于第二種,不知道有多少坑在等著我們呢!”
(碼農(nóng)翻身: 實際上這句話是托尼·霍爾說的)
老大不幸言中,終于有一天,這個復雜到看不出問題的系統(tǒng)崩潰了,這個世界又毀滅了。
第三世
第三代的旺財和小強從線程池出來。
出發(fā)前,Tomcat老大把前兩代線程遇到的問題給他們說了一遍,威脅說:如果再出現(xiàn)死鎖,小心你們兩個的腦袋!
旺財和小強戰(zhàn)戰(zhàn)兢兢,如履薄冰地執(zhí)行代碼。
最終他們還是遇到了傳說中的可怕的轉(zhuǎn)賬代碼:
- def transfer(from: Account, to: Account,amt:Int){
- atomic{
- from.withdraw(amt);
- to.deposit(amt);
- }
- }
旺財非常吃驚:“這是什么代碼?不是Java?”
小強說:“嗯,不是Java ,是Scala寫的,這是運行在JVM上的一個語言。”
(碼農(nóng)翻身注:實際上JVM線程能看到的只是Java 字節(jié)碼,根本看不到源碼,也就不知道是Java寫的代碼,還是Scala寫的代碼, 這里只是為了展示方便。)
旺財說:“怎么這么簡單,會不會出問題?那個atomic是怎么回事?表示原子執(zhí)行?”
小強也有點懵,不敢貿(mào)然去執(zhí)行:“咱們還是去問Tomcat老大吧。”
Tomcat看了一眼:“人類程序員又改代碼了啊,開始使用Software Transaction Memory(STM)了。 去把STM老頭兒叫來,讓他給你倆解釋。”
STM老頭兒滿臉滄桑:“放心執(zhí)行吧,只要你把代碼放到atomic中,我就能保證他們像事務一樣,實現(xiàn)ACID,哦不,D(持久化)實現(xiàn)不了,這些數(shù)據(jù)都是在內(nèi)存中的。”
“這有什么用? ”
“可以讓你們倆安全地并發(fā)執(zhí)行啊?”
旺財和小強面面相覷,這連鎖都沒有,還安全地并發(fā)?
“別看沒有鎖,” STM老頭兒說,“在atomic代碼開始執(zhí)行的時候,我會記錄下代碼塊涉及到的數(shù)據(jù)的值(復制了一份),然后才真正執(zhí)行,執(zhí)行完了要‘提交’, 這時候我會看看那些數(shù)據(jù)的值是否也被別的線程改動了,如果有改動,那本次改動就撤銷,重新從代碼開始處執(zhí)行。 ”
老頭兒畫了一個圖,展示旺財從賬戶A給賬戶B轉(zhuǎn)20元, 與此同時小強從B向A轉(zhuǎn)30元。
還真是,沒有加鎖就安全地完成了兩個并發(fā)操作。
當然,老頭兒為了實現(xiàn)這個atomic操作,背后偷偷做了不少事情:復制數(shù)據(jù),提交,重復執(zhí)行。
旺財想起來自己曾經(jīng)執(zhí)行過一下Java 的Compare and swap的代碼,說道:“你這不就是CAS嘛!”
老頭兒說:“原理上類似,都是樂觀鎖,不過我這個方式和數(shù)據(jù)庫的事務更加類似,所以叫做Software Transaction Memory。”
小強想了想,說道:“不對啊,atomic是個代碼塊,里邊可能有很多代碼,涉及到很多class, 你怎么知道哪些字段需要被STM管理起來啊!”
STM老頭兒說:“這真是個好問題,實際上,需要程序員們來告訴我。比如使用這個方法”
- class Account(val initialBalance : Int){
- val balance = Ref(initialBalance)
- ......
- }
“看到那個Ref沒有,這就是一種辦法,通過它,我就知道這個balance的字段需要讓我管理起來,在atomic代碼塊運行的時候,就需要復制它的值,比較它的值。”
“明白了,但是‘重復執(zhí)行’有問題啊,假設程序員張大胖是這么寫代碼的:”
- def transfer(from: Account, to: Account,amt:Int){
- atomic{
- from.withdraw(amt);
- ...在這里執(zhí)行一些其他操作,例如打印日志,發(fā)送郵件.....
- to.deposit(amt);
- }
- }
“這其中有一些打印日志,發(fā)送郵件的操作,那你重復執(zhí)行,豈不會執(zhí)行很多次,就完全亂套了。”
STM老頭兒說:“不錯,想得挺深,你說的這些操作,我把他們叫做副作用,不能重復執(zhí)行,不能放到atomic代碼塊中讓STM管理。換句話說atomic中的代碼應該是冪等的。如果違背了這一點,后果自負!”
小強心中一凜:“這是程序員要操心的事情了,不管我倆的事情, 不過即使如此,他們的代碼也極度地簡化了,只需要用個atomic,就能實現(xiàn)安全地并發(fā),實在是太爽了。”
旺財說道:“你說得天花亂墜,這STM有什么缺點?”
老頭兒說:“天下沒有免費的午餐,很容易想到STM的局限性, 如果對于同一個數(shù)據(jù),并發(fā)寫入很多的時候,沖突就大大增加了,不斷地重復執(zhí)行,效率很低。所以更適合寫入少,讀取多的場景。”
“好吧,我們這就執(zhí)行這個轉(zhuǎn)賬操作,有問題就找你!”
【本文為51CTO專欄作者“劉欣”的原創(chuàng)稿件,轉(zhuǎn)載請通過作者微信公眾號coderising獲取授權】