Java併發編程-用鎖的正確姿式:爲何加了鎖,但餘額仍是出錯?

在 Java 中,鎖好像是顆萬能藥,沒什麼問題是加鎖解決不了的。的確,鎖能解決絕大部分的併發問題。編程

然而,最簡單的東西也每每最容易出現問題。你只要稍有不慎,不但 Bug 沒有解決,還得花費大量的時間作各類排查。segmentfault

既然如此,咱們就來好好看看:爲何用了鎖,程序仍是出錯了。併發

什麼纔是正確的鎖模型

一說到鎖,你的大腦中可能立馬想起這個模型。性能

簡易鎖模型

中間藍色的一段代碼,叫作:臨界區。線程進入臨界區前,先嚐試加鎖,若是成功,就進入臨界區。此時,這個線程持有鎖,等執行完臨界區的代碼後,持有鎖的線程就會執行解鎖this

一樣的道理,若是加鎖失敗,線程就會一直等待,直到持有鎖的線程解鎖後,再從新嘗試加鎖。spa

這是鎖的簡易模型,看起來簡單明瞭,但這個模型是錯的,它忽略了最最重要的一點:鎖與資源的關係線程

你還記得嗎?鎖是爲了實現互斥,即:在同一時刻,一個資源只能由一個線程操做。3d

換句話說,之因此要用鎖,就是要保護某些資源。所以,鎖與資源之間的關係,是重點中的重點。不少併發 Bug 就是忽略了這一點致使的。好比,你看下面這段代碼:code

class Account {
    private int balance;

    // 轉帳
    synchronized void transfer(Account target, int amt){
        if (this.balance > amt) {
            this.balance -= amt;
            target.balance += amt;
        }
    }
}

你可能會以爲,這段代碼沒問題呀?transfer() 方法不是加鎖了嗎?但在併發環境下,轉帳後,餘額極可能對不上。緣由也很簡單,沒考慮鎖和資源的關係。對象

既然如此,那正確的鎖模型是怎樣的呢?

改進鎖模型

首先,咱們要標註出受保護的資源 R;而後,爲資源 R 建立一把鎖 LR;最後,在進出臨界區的時候,再加上加鎖、解鎖操做。

其中,鎖 LR受保護資源 R 之間有一條關聯線,這很是重要,表明的是:鎖與資源之間,是有對應關係的。

打個比方,你家的鎖只能保護你家的東西,我家的鎖只能保護我家的東西。這個關係要搞清楚,否則,就是鎖自家的門來保護他家的東西。

能夠說,咱們平時對鎖的理解都是錯的。雖然這通常不會有什麼問題,但只要出現問題,就是公司破產之類的大事。

好比,我前東家的支付系統,就是由於鎖的問題,鉅虧了幾十萬,負責人引咎辭職。在瀕臨倒閉之際,我接手了,這個項目才起死回生。

因此,若是你不想重蹈覆轍,而是想升職加薪。那必需要掌握正確的鎖模型,千萬不能忽略鎖和資源的關係

鎖與資源的關係

你已經知道了,想要用好用鎖,關鍵是搞清楚鎖和資源的關係。那麼,這二者的關係是怎樣的呢?

資源和鎖之間的關係是 N:1。

換句話說,你能夠用一把鎖,來保護多個資源。可是,你不能用多把鎖,來保護一個資源。緣由在於,若是你針對一個資源建立了多把鎖,那麼就達不到互斥的效果了。

打個比方,現實世界中,你能夠用好幾把鎖,來保護你家的東西。但在編程世界中,你不能這樣作。你看這個例子:

class SafeCalc {
    private long value = 0L;

    void addOne() {
        synchronized (this) {
            value += 1;
        }
    }

    void addTwo() {
        synchronized (SafeCalc.class) {
            value += 2;
        }
    }
}

你看下 addOne()addTwo() 兩個方法,它們分別用了兩把鎖 thisSafeCalc.class。雖然都是保護同一個資源,但臨界區被拆成了 2 個,而這 2 個臨界區是沒有互斥關係的,這致使了併發問題。

鎖關係

那問題該怎麼解決呢?

很簡單,不要用多把鎖來保護一個資源,你能夠只用 this,又或者只用 SafeCalc.class

固然,這個例子比較簡單。在現實工做中,狀況確定更加複雜,咱們要保護的資源不止一個,而是多個,這又該怎麼處理呢?

如何保護多個資源

不管是單個資源,仍是多個資源,關鍵都在於:鎖要覆蓋全部受保護的資源。

單個資源比較好理解,但若是要保護的資源不止一個,那咱們首先要作的是,區分這些資源是否有關聯,這分爲兩種狀況。

第一,如何保護沒有關聯的多個資源,這和處理單個資源差很少。若是不考慮性能,你能夠用一把鎖來保護;若是想提升性能,你能夠用不一樣的鎖來保護。

你看下面的代碼:

class Account {
    // 餘額
    private Integer balance;
    // 密碼
    private String password;

    // 鎖:保護餘額
    private final Object balLock = new Object();
    // 鎖:保護密碼
    private final Object pwLock = new Object();

    // 取款
    void withdraw(Integer amt) {
        synchronized (balLock) {
            if (this.balance > amt) {
                this.balance -= amt;
            }
        }
    }

    // 更改密碼
    void updatePassword(String pw) {
        synchronized (pwLock) {
            this.password = pw;
        }
    }
}

帳戶類-Account有兩個方法:取款-withdraw()更改密碼-updatePassword()。從功能上看,這兩個方法沒什麼關聯。因此,爲了提高性能,我用了兩把鎖,讓它們各管各的。

固然,你也能夠用一把鎖來管理全部資源,就像下面這樣:

class Account {
    // 餘額
    private Integer balance;
    // 密碼
    private String password;

    // 取款
    synchronized void withdraw(Integer amt) {
        if (this.balance > amt) {
            this.balance -= amt;
        }
    }

    // 更改密碼
    synchronized void updatePassword(String pw) {
        this.password = pw;
    }
}

在這裏,我對性能沒有要求,因此只要用 this 這一把鎖,直接管理帳戶的全部資源。

第二,如何保護有關聯的多個資源,這個問題就有點複雜了。

好比,銀行的轉帳操做,不一樣的帳戶是有關聯的,帳戶 A 若是減小 100 元,帳戶 B 就得增長 100 元。這時候,該怎麼避免轉帳的併發問題呢?

你可能想到加鎖,用 synchronized 關鍵詞修飾一下,就像下面這樣:

class Account {
    // 餘額
    private Integer balance;

    // 轉帳
    synchronized void transfer(Account target, Integer amt) {
        if (this.balance > amt) {
            this.balance -= amt;
            target.balance += amt;
        }
    }

}

一把鎖能夠保護多個資源,這看上去沒問題。然而,倒是錯誤的作法。

你想象一下這樣的場景,A、B、C 三個帳戶的餘額都是 100 元。這時候,有兩筆轉帳操做:帳戶 A 轉 100 元到帳戶 B,帳戶 B 轉 100 元到帳戶 C。照理說,結果應該是:帳戶A-0元,帳戶B-100元,帳戶C-200元。

然而,若是是併發轉帳,最終的結果還有兩種可能。一種是:帳戶A-0元,帳戶B-200元,帳戶C-200元;另外一種是:帳戶A-0元,帳戶B-0元,帳戶C-200元。

image

簡單來講,帳戶 B 的餘額極可能是錯的,問題就出在this這把鎖上。

還記得嗎?鎖要覆蓋全部受保護的資源。

在這個例子中,卻沒能作到這一點。臨界區內有兩個資源:this.balancetarget.balance。但this這把鎖只能保護this.balance,不能保護target.balance

image

打個比方,你家的鎖就算再厲害,也無法保護鄰居家的東西吧?

所以,咱們要找到一把鎖,同時覆蓋全部帳號。

方案仍是挺多的,最簡單的一個就是:Account.classAccount.class 共享給全部 Account 對象。並且,Account.class 由 Java 虛擬機建立,具備惟一性。

image

這樣一來,轉帳的併發問題就解決了,代碼也特別簡單。

class Account {
    // 餘額
    private Integer balance;

    // 轉帳
    void transfer(Account target, Integer amt) {
        synchronized (Account.class) {
            if (this.balance > amt) {
                this.balance -= amt;
                target.balance += amt;
            }
        }
    }
}

寫在最後

在 Java 中,鎖是解決併發的萬能藥,但咱們卻每每用很差,這是由於咱們忽略了鎖與資源的關係

通常狀況下,這不會有什麼大問題。

然而,一旦出現多個資源,這些資源還相互關聯,就極可能出現公司破產之類的大事。所以,你要時刻記住 3 點:

  1. 鎖與資源有對應關係;
  2. 資源和鎖之間的關係是 N:1;
  3. 鎖要覆蓋全部受保護的資源;

你只要檢查好這三點,壞事就輪不到你頭上。

相關文章
相關標籤/搜索