Java併發編程-死鎖(上):追求性能的代價

前面幾篇文章,咱們一直在關注如何解決併發問題,也就是程序的原子性、可見性、有序性。這些問題一旦出現,程序的結果就無法保證。程序員

好在 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 這個資源覆蓋的範圍實在太大了。

並且,帳戶不止轉帳一個功能,還有查餘額、提現等等操做,可這些操做也得串行處理的,那性能天然好不了。

那這樣行不行?既然問題是鎖覆蓋的範圍太大,那我縮小覆蓋範圍,問題不就解決了嗎?

很是正確,這樣的鎖叫:細粒度鎖

所謂細粒度鎖,就是縮小資源的覆蓋範圍,而後用不一樣的鎖對資源作精細化管理,從而提升程序的並行度,以此來提高性能。

那按照這個思路,咱們來分析一下代碼,轉帳只涉及到兩個帳戶,分別是:thistarget。既然如此,咱們就不用鎖定整個 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、線程二都在死死地等着,這就是經典的死鎖問題了,你能夠看下這副圖。

死鎖的資源分佈

在這副圖中,兩個線程造成一個完美的閉環,根本無法出去。

並且,轉帳隨時都會發生,但這兩個線程卻一直佔着資源,新的訂單無法處理,只能堆在一塊兒,愈來愈多。這不只浪費大量的計算機資源,還影響其它功能的運行。

此外,程序一旦發生死鎖,那除了重啓應用外,沒有別的路能走。但銀行分分鐘進出幾十億,重啓應用也是死路一條。

能夠說,如何完全解決死鎖問題,就是程序員價值所在。

寫在最後

鎖的本質是串行化,若是鎖覆蓋的範圍太大,會致使程序的性能低下。

爲了提高性能,咱們用了細粒度鎖,但這又帶來了死鎖問題。

既然如此,死鎖該怎麼解決?咱們下次再聊。

相關文章
相關標籤/搜索