一個故事搞懂Java併發程式設計

閃客sun發表於2019-06-13

  最近在給別人講解Java併發程式設計面試考點時,為了解釋鎖物件這個概念,想了一個形象的故事。後來慢慢發現這個故事似乎能講解Java併發程式設計中好多核心概念,於是完善起來形成了了這篇文章。大家先忘記併發程式設計,只聽我給你講個故事。

 

  故事可能比較奇怪。有這麼一個學校,裡面有好多好多人,我們簡單分成學生老師、以及宿管阿姨。學校中間還有一個很奇葩的水果超市,裡面有個倉庫放著蘋果西瓜橘子。來這個超市的人,一方面可以拿走水果吃掉,另一方面也可以送來水果還錢。不過超市還有一個很奇葩的規則,就是學生只能去吃或者送蘋果,老師則只能西瓜,宿管阿姨只能橘子。

  這個超市的進出也很有規矩,來這個超市的人,必須持有相應的證件,學生則需要持有學生證,老師需要持有教師證,宿管阿姨需要持有阿姨證。這三個證每個都分別只有一個,保管在超市門口的一個領證處,人們進入這個超市之前,必須先去取證處那裡領取相應的證件才能進入。如果證件暫時被別人取走了拿不到,則需要到後方的等待區裡面排隊等證。那這個等待區也有三個,分別是學生證等待區,教師證等待區,阿姨證等待區。

  進入超市裡面就更加奇葩了,不論是要從這個超市拿走水果,還是要送來水果,都需要通過一個操作檯來控制,而這個操作檯,同一時刻只能有一個人進行操作。這個操作檯為了防止有人霸佔操作檯過長時間,只允許一個人持續操作10s鍾,10s之後會在螢幕上顯示一個ID,只有這個ID的人才能來操作,至於選擇什麼號碼,老師學生或是宿管阿姨都無法決定和干預,只能任憑這個操作檯來決策。但好在,每個人在操作檯上都有自己的賬號,操作一半被中斷的資料並不會丟失。

  這個故事的背景就介紹完了,下面這個學校就發生了各種各樣的事。

  首先我們假設,進這所學校的人,都是為了去超市做事情。某一時刻,操作檯上顯示了一個號碼2號,這個號碼通過各種學校大螢幕通知給所有的人。於是ID為2號的學生小明看到了自己的號碼,得知自己獲得了進入超市操作控制檯的權利,於是出發前往超市。小明首先到超市門口,問領證處的管理人員,“給我一張學生證!”。管理人員找了找發現有一張學生證,於是便給了小明。小明拿到了學生證,順利進入超市,並坐上了操作檯前,登入了自己的賬號系統。小明此行的目的是為了拿走一個蘋果,於是他點選了蘋果商品的圖示,系統顯示蘋果還有4個。於是小明順利地拿走了蘋果,系統將蘋果數量-1,將新的蘋果數量3記錄到總系統庫中。接著小明走出超市,將學生證交還給了領證處,走出了校園,消失在外面的人海中。

  接著操作檯上顯示了3號,同樣通過學校大螢幕通知給了所有人。ID為3號的學生小張看到了自己的號碼,得知自己獲得了進入超市操作控制檯的權利,於是出發前往超市。小張和小明做著完全相同的操作,但小張操作太慢了,剛剛點選完了蘋果商品的圖示,系統就顯示了下一個人的號碼5號。此時小明只能被迫終止自己的操作,讓出操作檯的權利。ID為5號的學生小王接到通知,興沖沖地前往超市,並在領證處問管理人員,“給我一張學生證!”。管理人員找了找,發現學生證已經被小明取走了,只能告訴小王,“抱歉,學生證暫時沒有,請到後面的學生證等待區排隊吧!”。小王沒辦法,只能乖乖去排隊了。

  這是操作檯再次顯示了2號,也就是剛剛操作到一半的小明。小明此時還在超市裡,並不需要重新進入,於是小明趕緊到操作檯前繼續著剛剛的操作,取走了一個蘋果,離開了超市,交還了學生證。此時領證處的管理人員收到了學生證,對著後面的學生證排隊區喊,“學生證有啦,排隊的人過來取吧!”。正在排隊等證的5號小王聽到後,從排隊的佇列裡出來,準備領證並進入超市。但此時操作檯上顯示的號是另一個學生10號,10號學生拿走了學生證,進入超市開始操作。操作到一半,操作檯時間限制又到了,顯示了小王的ID5號。小王剛從等待領證的佇列裡出來,終於獲得了進行下一步行動的准許,於是走向了領證處,“給我一張學生證!”,由於學生證已經被10號拿走,管理人員只能說,“抱歉,學生證暫時沒有,請到後面的學生證等待區排隊吧!”。小王一看等了那麼久居然又被別人搶先了一步,剛想爆粗口,想到了這個學校的名言,“這個世界是不公平的”,於是又乖乖走向了學生證等待區,繼續排隊。

  等10號操作完出來了,還了學生證,小王又被領證處管理員喊話,“學生證有啦,排隊的人過來取吧!”。小王走出排隊區,而此時操作檯終於顯示了小王的號碼5號。小王這次順利領取了學生證,進入了超市,坐在了操作檯上,登入了自己的系統。小王想買蘋果,於是點選了蘋果商品的按鈕,但系統顯示蘋果數量為0!小王此時想了想,有了個接下來的計劃:

  1. 繼續呆在超市裡,得空就去操作檯上查詢一下蘋果的數量,直到有蘋果為止。但繼續呆在超市裡,可能導致想向超市送蘋果的學生拿不到學生證,而自己也就永遠無法得到蘋果了,顯然不妥。
  2. 所以小王的另一個想法是,走出超市,交還學生證,等下次有機會再進入超市檢視蘋果數量,直到有蘋果為止。這樣雖然有機會得到蘋果,但太累了,假如這期間根本沒人往超市送蘋果,那這一趟趟其實是白費事的。
  3. 於是小王想出了一個聰明的方案,我可以走出超市,到一個地方等待,在這裡不會收到操作檯的通知。如果有人向超市送蘋果了,那這個等待區裡會發一個訊號,這時超市才有可能是有蘋果的,這時我從等待區裡出來,等待叫號的機會。雖然蘋果有可能被其他吃蘋果的學生搶沒,但這樣起碼不會浪費太多時間。

  剛剛好超市旁邊為每一種水果準備了好多等待區,一共有六個,分別是:蘋果沒了等待區,西瓜沒了等待區,橘子沒了等待區。蘋果滿了等待區,西瓜滿了等待區,橘子滿了等待區。小王很聰明,去了蘋果沒了等待區,等待著有人往裡送蘋果的訊號。

  這時小孫走進了超市,給超市添置了5個蘋果,並換來了零花錢。之後他立刻通知蘋果沒了等待區,給了個訊號“超市有蘋果啦!”,但此時小孫還沒有走出超市呢。小王在等待區裡收到訊號,立刻走出了等待區,等待被叫號,以完成自己吃蘋果的任務。但很不幸,在小王得到叫號機會之前,蘋果又被其他幾個學生搶光了,這時才輪到小王。小王也很聰明,他考慮到了這種情況,沒有直接取蘋果,而是重新查詢了一變蘋果數量,發現蘋果數量為0,於是重複之前的步驟,小王再次回到了蘋果沒了等待區。

  接下來的時間裡,小王不斷在蘋果沒了等待區和學生證等待區移動,小王發現為了吃一個蘋果太難了,必須同時滿足,蘋果沒了等待區發來了“超市有蘋果了”的訊號,領證區此時有學生證,並且在操作檯上查詢出的蘋果數量不為0。終於有一次。小王成功滿足了這三個條件,在操作檯上看到蘋果的數量為1!小王正激動地準備按下購買按鈕,可此時操作檯一閃,突然出現了別人的號碼。這個人是超市管理員,拿著一張特殊的超市管理員證順利進入了超市,將蘋果拿走,此時蘋果數量又變成了0。之後又輪到小王操作,但小王並不知道之前發生的一切,他眼中明明看到蘋果數量是1。小王為了保險起見,又多次查詢了蘋果數量,發現仍然是1,於是興奮地點下了購買按鈕!於是,操作檯對根本沒有蘋果的儲藏區發出了取蘋果的指令,該系統根本沒有想到會有這種事情發生,於是機器炸了,整個學校夷為平地。

  數年後,學校慢慢被重新建立了起來,之前做操作檯的人已經被槍斃了,高薪聘請了一位高人來建造,解決了之前的那個問題。超市又順利運轉起來,有時超市只有一個人,有時超市會有三個人,分別是學生、老師、宿管阿姨,他們仨人互不影響,相安無事。學校的生活再次豐富了起來。

 

----------------------華麗的分割線-----------------------

 

  這個故事包含了Java多執行緒的大部分核心問題,下面我把故事重新講一遍。

  有這麼一個學校(Java虛擬機器),裡面有好多好多(執行緒),我們簡單分成學生老師、以及宿管阿姨。學校中間還有一個很奇葩的水果超市(臨界區),裡面有個倉庫放著蘋果西瓜橘子(臨界區裡的受保護資源)。來這個超市的人,一方面可以拿走水果吃掉,另一方面也可以送來水果還錢。不過超市還有一個很奇葩的規則,就是學生只能去吃或者送蘋果,老師則只能西瓜,宿管阿姨只能橘子。

  這個超市的進出也很有規矩,來這個超市的人,必須持有相應的證件(鎖物件),學生則需要持有學生證,老師需要持有教師證,宿管阿姨需要持有阿姨證(不同的鎖物件)。這三個證每個都分別只有一個,保管在超市門口的一個領證處(獲取鎖的地方--可以說是堆吧),人們進入這個超市之前,必須先去取證處那裡領取相應的證件(獲取鎖)才能進入。如果證件暫時被別人取走了拿不到(獲取鎖失敗),則需要到後方的等待區(同步佇列SychronizedQueue)裡面排隊等證。那這個等待區也有三個,分別是學生證等待區,教師證等待區,阿姨證等待區(每個鎖物件對應一個同步佇列)

  進入超市裡面就更加奇葩了,不論是要從這個超市拿走水果,還是要送來水果,都需要通過一個操作檯(單核CPU)來控制,而這個操作檯,同一時刻只能有一個人進行操作。這個操作檯為了防止有人霸佔操作檯過長時間,只允許一個人持續操作10s鍾(CPU時間片),10s之後會在螢幕上顯示一個ID,只有這個ID的人才能來操作(執行緒切換)。至於選擇什麼號碼,老師學生或是宿管阿姨都無法決定和干預,只能任憑這個操作檯來決策(作業系統決定執行緒的切換和時間的分配)。但好在,每個人在操作檯上都有自己的賬號(執行緒的工作記憶體),操作一半被中斷的資料並不會丟失。

  這個故事的背景就介紹完了,下面這個學校就發生了各種各樣的事。

  首先我們假設,進這所學校的人,都是為了去超市做事情。首先人出現在學校外(執行緒狀態NEW),人進入學校(執行緒狀態RUNNABLE)。某一時刻,操作檯上顯示了一個號碼2號,這個號碼通過各種學校大螢幕通知給所有的人。於是ID為2號的學生小明看到了自己的號碼,得知自己獲得了進入超市操作控制檯的權利(獲得CPU執行權),於是出發前往超市。小明首先到超市門口,問領證處的管理人員,“給我一張學生證!”(獲取鎖)。管理人員找了找發現有一張學生證,於是便給了小明。小明拿到了學生證,順利進入超市(獲取鎖成功,進入臨界區),並坐上了操作檯前,登入了自己的賬號系統(準備好工作記憶體,開始執行臨界區程式碼)。小明此行的目的是為了拿走一個蘋果,於是他點選了蘋果商品的圖示,系統顯示蘋果還有4個。於是小明順利地拿走了蘋果,系統將蘋果數量-1,將新的蘋果數量3記錄到總系統庫中(程式碼)。接著小明走出超市(程式碼執行完畢出臨界區),將學生證交還給了領證處(釋放鎖),走出了校園(執行緒狀態TERMINAL),消失在外面的人海中。

  接著操作檯上顯示了3號,同樣通過學校大螢幕通知給了所有人。ID為3號的學生小張看到了自己的號碼,得知自己獲得了進入超市操作控制檯的權利,於是出發前往超市。小張和小明做著完全相同的操作,但小張操作太慢了,剛剛點選完了蘋果商品的圖示,系統就顯示了下一個人的號碼5號。此時小明只能被迫終止自己的操作,讓出操作檯的權利(執行緒切換)。ID為5號的學生小王接到通知,興沖沖地前往超市,並在領證處問管理人員,“給我一張學生證!”。管理人員找了找,發現學生證已經被小明取走了,只能告訴小王,“抱歉,學生證暫時沒有,請到後面的學生證等待區(同步佇列WaitQueue)排隊吧!”(獲取鎖失敗)。小王沒辦法,只能乖乖去排隊了(執行緒狀態BLOCKING)

  這是操作檯再次顯示了2號,也就是剛剛操作到一半的小明。小明此時還在超市裡(不釋放鎖),並不需要重新進入,於是小明趕緊到操作檯前繼續著剛剛的操作(執行緒切換,繼續執行中斷的程式碼),取走了一個蘋果,離開了超市,交還了學生證(釋放鎖)。此時領證處的管理人員收到了學生證,對著後面的學生證排隊區喊,“學生證有啦,排隊的人過來取吧!”(通知同步佇列出隊)。正在排隊等證的5號小王聽到後,從排隊的佇列裡出來,準備領證並進入超市。但此時操作檯上顯示的號是另一個學生10號,10號學生拿走了學生證,進入超市開始操作。操作到一半,操作檯時間限制又到了,顯示了小王的ID5號。小王剛從等待領證的佇列裡出來,終於獲得了進行下一步行動的准許,於是走向了領證處,“給我一張學生證!”,由於學生證已經被10號拿走,管理人員只能說,“抱歉,學生證暫時沒有,請到後面的學生證等待區排隊吧!”。小王一看等了那麼久居然又被別人搶先了一步,剛想爆粗口,想到了這個學校的名言,“這個世界是不公平的”,於是又乖乖走向了學生證等待區,繼續排隊。(非公平鎖,並不是誰等的時間最長誰就獲取鎖)

  等10號操作完出來了,還了學生證,小王又被領證處管理員喊話,“學生證有啦,排隊的人過來取吧!”。小王走出排隊區,而此時操作檯終於顯示了小王的號碼5號。小王這次順利領取了學生證,進入了超市,坐在了操作檯上,登入了自己的系統。小王想買蘋果,於是點選了蘋果商品的按鈕,但系統顯示蘋果數量為0!小王此時想了想,有了個接下來的計劃:

  1. 繼續呆在超市裡,得空就去操作檯上查詢一下蘋果的數量,直到有蘋果為止。但繼續呆在超市裡,可能導致想向超市送蘋果的學生拿不到學生證,而自己也就永遠無法得到蘋果了,顯然不妥。(sychronized程式碼塊裡迴圈等待)
  2. 所以小王的另一個想法是,走出超市,交還學生證,等下次有機會再進入超市檢視蘋果數量,直到有蘋果為止。這樣雖然有機會得到蘋果,但太累了,假如這期間根本沒人往超市送蘋果,那這一趟趟其實是白費事的。(sychronized程式碼塊外迴圈等待)
  3. 於是小王想出了一個聰明的方案,我可以走出超市,到一個地方等待(wait),在這裡不會收到操作檯的通知。如果有人向超市送蘋果了,那這個等待區裡會發一個訊號(notify),這時超市才有可能是有蘋果的,這時我從等待區裡出來,等待叫號的機會。雖然蘋果有可能被其他吃蘋果的學生搶沒,但這樣起碼不會浪費太多時間。(等待通知機制)

  剛剛好超市旁邊為每一種水果準備了好多等待區(等待佇列WaitQueue),一共有六個,分別是:蘋果沒了等待區,西瓜沒了等待區,橘子沒了等待區。蘋果滿了等待區,西瓜滿了等待區,橘子滿了等待區(條件變數Condition)。小王很聰明,走出超市交還學生證(wait會釋放鎖),去了蘋果沒了等待區(wait),等待著有人往裡送蘋果的訊號(同步訊號-喚醒)

  這時小孫走進了超市,給超市添置了5個蘋果,並換來了零花錢。之後他立刻通知蘋果沒了等待區,給了個訊號“超市有蘋果啦!(AppleNotEmpty.notifyAll)”,但此時小孫還沒有走出超市呢(notify不釋放鎖)。小王在等待區裡收到訊號,立刻走出了等待區,等待被叫號,以完成自己吃蘋果的任務。但很不幸,在小王得到叫號機會之前,蘋果又被其他幾個學生搶光了,這時才輪到小王。小王也很聰明,他考慮到了這種情況,沒有直接取蘋果,而是重新查詢了一變蘋果數量(wait一般配合while條件),發現蘋果數量為0,於是重複之前的步驟,小王再次回到了蘋果沒了等待區。

  接下來的時間裡,小王不斷在蘋果沒了等待區和學生證等待區移動,小王發現為了吃一個蘋果太難了,必須同時滿足,蘋果沒了等待區發來了“超市有蘋果了”的訊號,領證區此時有學生證,並且在操作檯上查詢出的蘋果數量不為0。終於有一次。小王成功滿足了這三個條件,在操作檯上看到蘋果的數量為1!小王正激動地準備按下購買按鈕,可此時操作檯一閃,突然出現了別人的號碼。這個人是超市管理員,拿著一張特殊的超市管理員證順利進入了超市,將蘋果拿走,此時蘋果數量又變成了0。之後又輪到小王操作,但小王並不知道之前發生的一切,他眼中明明看到蘋果數量是1。小王為了保險起見,又多次查詢了蘋果數量,發現仍然是1(非volatile修飾的變數不保證執行緒之間的可見性),於是興奮地點下了購買按鈕!於是,操作檯對根本沒有蘋果的儲藏區發出了取蘋果的指令,該系統根本沒有想到會有這種事情發生,於是機器炸了,小王犧牲(丟擲執行時異常,執行緒釋放鎖並終止)

  數年後,之前做操作檯的人已經被槍斃了,學校又高薪聘請了一位高人來建造,解決了之前的那個問題(volatile)。超市又順利運轉起來,有時超市只有一個人(不同執行緒進入鎖物件相同的臨界區會互斥,只有一個執行緒可以進入),有時超市會有三個人(不同鎖物件的臨界區不互斥),分別是學生、老師、宿管阿姨,他們仨人互不影響,相安無事。學校的生活再次豐富了起來。

 

  故事講完了,雖然不能解釋全部併發程式設計的內容,也不能處處都很恰當地說明細節,但確是一個很有趣的思考過程,希望大家也能積極討論下故事中的錯誤和不完善的地方,一起將故事講的更好。下面整理一下故事中出現的東西和寓意。

東西 寓意
執行緒
通行證 鎖物件
水果超市 臨界區程式碼
水果 受保護資源
操作檯 CPU
叫號 時間片分配
領證處 獲取鎖
等待區 等待佇列
領證排隊區 同步佇列
水果儲藏區 主記憶體
每個人的賬號系統 工作記憶體

 

相關文章