前面幾篇文章,咱們一直在關注如何解決併發問題,也就是程序的原子性、可見性、有序性。這些問題一旦出現,程序的結果就無法保證。程序員
好在 Java 是一門強大的語言,鎖-synchronized
是一味萬能藥。你只要用好鎖,幾乎能解決全部併發問題。編程
不過,併發編程有一個特色:解決完一個問題,總會冒出另外一個新問題。segmentfault
實際開發中,鎖雖然是一副萬能藥,但使用起來要很是當心。由於你不但要考慮鎖和資源的關係,還得考慮性能問題。多線程
咱們之因此寫併發程序,不就是想提升性能嗎?併發
然而,鎖的本質是串行化,程序要排隊輪流執行。這樣一來,多線程的優點就無法發揮了,性能天然會降低。性能
好比,銀行的轉帳操做,若是想保證結果的正確,就得用到鎖。優化
class Account { // 餘額 private Integer balance; // 轉帳 void transfer(Account target, Integer amt) { synchronized (Account.class) { if (this.balance > amt) { this.balance -= amt; target.balance += amt; } } } }
在這裏,咱們對 Account.class
進行加鎖,解決了銀行轉帳的併發問題,代碼也特別簡單,看似很完美。可是,這裏有一個致命缺陷:性能太差,全部帳戶的轉帳操做都是串行的。this
在現實世界中,帳戶 A 轉帳戶 B,帳戶 C 轉帳戶 D,這些都是能夠並行處理的。但在這個方案中,卻沒考慮這些,轉帳只能一筆一筆的處理。spa
試想一下,中國網民即便天天只交易一次,就有 10 億筆轉帳,平均每秒轉帳超過 1 萬次。若是交易只能一筆筆處理,那結果是對了,性能卻根本無法看。線程
所以,在實際工做中,咱們不光要考慮程序的結果對不對,還得考慮程序的性能好很差。
咱們曾提到過,若是想用好鎖,那麼鎖要覆蓋全部受保護的資源。否則的話,就無法發揮鎖的互斥做用。PS.能夠複習這篇文章:用鎖的正確姿式
然而,若是鎖覆蓋的範圍太大,程序的性能也會大幅降低。
好比,前面的轉帳操做實在是牽連巨大,你再看一下代碼:
class Account { // 餘額 private Integer balance; // 轉帳 void transfer(Account target, Integer amt) { synchronized (Account.class) { if (this.balance > amt) { this.balance -= amt; target.balance += amt; } } } }
每筆轉帳只涉及了兩個帳號,但咱們卻把整個 Account.class
鎖住了。這個方案雖然簡單,但 Account.class
這個資源覆蓋的範圍實在太大了。
並且,帳戶不止轉帳一個功能,還有查餘額、提現等等操做,可這些操做也得串行處理的,那性能天然好不了。
那這樣行不行?既然問題是鎖覆蓋的範圍太大,那我縮小覆蓋範圍,問題不就解決了嗎?
很是正確,這樣的鎖叫:細粒度鎖。
所謂細粒度鎖,就是縮小資源的覆蓋範圍,而後用不一樣的鎖對資源作精細化管理,從而提升程序的並行度,以此來提高性能。
那按照這個思路,咱們來分析一下代碼,轉帳只涉及到兩個帳戶,分別是:this
、target
。既然如此,咱們就不用鎖定整個 Account.class
,只須要鎖定兩個帳戶,作兩次加鎖操做就行了。
首先,咱們嘗試鎖定轉出帳戶 this
;而後,再嘗試鎖定轉入帳戶 target
。只有兩個帳戶都鎖定成功時,才能執行轉帳操做。你能夠看下面這副圖:
思路有了,接下來,就得轉換成代碼了。
class Account { // 餘額 private Integer balance; // 轉帳 void transfer(Account target, Integer amt) { synchronized (this) { synchronized (target) { if (this.balance > amt) { this.balance -= amt; target.balance += amt; } } } } }
相比原來的代碼,如今只是多用了一個 synchronized
,好像沒有什麼變化,但轉帳的並行度卻大大提升。
你想一下,如今同時出現兩筆交易,帳戶 A 轉帳戶 B,帳戶 C 轉帳戶 D。
本來的轉帳操做是鎖定了整個 Account.class
,轉帳只能一筆一筆地處理。
但通過改造後,第一筆轉帳只鎖定了 A、B 兩個帳戶,第二筆轉帳只鎖定了 C、D 兩個帳戶,這兩筆轉帳徹底能夠並行處理。
這樣一來,程序的性能提高了好幾個檔次,而這都是細粒度鎖的功勞。
在轉帳這個例子中,咱們一開始用是 Account.class
來作鎖,但爲了優化性能,咱們用了細粒度鎖,只鎖定和轉帳相關的兩個帳號。這樣一來,性能有了很大的提高。
然而,天下沒有免費的午飯。細粒度鎖看上去這麼簡單,是否是也有代價呢?
沒錯,細粒度鎖可能形成死鎖。所謂死鎖,就是兩個以上的線程在執行的時候,由於競爭資源形成互相等待,從而進入「永久」阻塞的狀態。
聽起來有點複雜,咱們仍是繼續看轉帳的例子吧。
class Account { // 餘額 private Integer balance; // 轉帳 void transfer(Account target, Integer amt) { synchronized (this) { synchronized (target) { if (this.balance > amt) { this.balance -= amt; target.balance += amt; } } } } }
如今同時有兩筆交易,帳戶A 轉 帳戶B,帳戶B 轉 帳戶A。這個就有了兩個線程,分別是:線程1、線程二。
其中,線程一先鎖定了帳戶A,線程二先鎖定了帳戶B。
那麼,問題來了。線程一想繼續對帳戶B 加鎖,但發現帳戶B 被鎖定,轉帳無法執行下去,只能進入等待。
一樣的道理,線程二想繼續對帳戶A 加鎖,但發現帳戶A 被鎖定,轉帳也無法執行,也進入了等待。
這樣一來,線程1、線程二都在死死地等着,這就是經典的死鎖問題了,你能夠看下這副圖。
在這副圖中,兩個線程造成一個完美的閉環,根本無法出去。
並且,轉帳隨時都會發生,但這兩個線程卻一直佔着資源,新的訂單無法處理,只能堆在一塊兒,愈來愈多。這不只浪費大量的計算機資源,還影響其它功能的運行。
此外,程序一旦發生死鎖,那除了重啓應用外,沒有別的路能走。但銀行分分鐘進出幾十億,重啓應用也是死路一條。
能夠說,如何完全解決死鎖問題,就是程序員價值所在。
鎖的本質是串行化,若是鎖覆蓋的範圍太大,會致使程序的性能低下。
爲了提高性能,咱們用了細粒度鎖,但這又帶來了死鎖問題。
既然如此,死鎖該怎麼解決?咱們下次再聊。