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