Java併發編程-階段性總結:安全性問題

經過前面的 7 篇文章,你可能以爲併發編程很複雜,既要考慮程序結果是否是正確,又要考慮程序能不能執行,還要考慮服務器能不能扛住,實在不知道從哪入手~編程

別怕,雖然看起來不少,但這實際上是一個打怪升級的過程,裏面有 3 道關卡:安全性問題、活躍性問題、性能問題,每一關都能有收穫,若是三關全過了,也就掌握了這門高階技能了。segmentfault

今天,咱們先過第一個關:安全性問題。緩存

安全性問題

所謂安全性問題,是指程序有沒有按照咱們的期待執行,就是正確性。相信你在工做的時候,必定會被人問到:這方法有沒有實現線程安全?這個類是否是線程安全的?安全

那什麼是線程安全呢?線程安全的本質也是正確性,程序按照咱們的指望執行。具體來講,不管在單線程環境下,仍是在多線程環境下,最終的運行結果都是同樣的,不會隨着環境而變化。服務器

一個程序要想實現線程安全,就必須避免這麼三個問題,分別是:可見性問題、有序性問題、原子性問題。關於這幾個問題,能夠看下之前的文章:Java併發編程-併發根源,裏面詳細講了這三個問題的來龍去脈。多線程

在弄清問題的前因後果後,咱們得動手解決問題了。然而,一說到併發編程,各路大神就嗨起來了,各類專業術語,各類解讀源碼,就是不提怎麼寫代碼。併發

但其實,實現線程安全,寫好一個多線程應用沒那麼難。你能夠看看這篇文章,Java併發編程-解決併發,裏面就是爲了破除你的畏難情緒,從實際工做出發,一個個的解決問題。編程語言

問題的緣由都搞清楚了,解決方案也有了。那是否是要仔細檢查全部代碼,百分百保證線程安全呢?性能

固然不是,實現線程安全的成本很高,須要耗費你大量的時間精力。並且,併發編程畢竟是高階技能,這就意味着,這項技能雖然很是重要,但使用場景卻不多。this

只有在共享數據會變化的狀況下,才須要實現線程安全。具體來講,就是隻有在多個線程同時讀寫一個數據時,你才須要去考慮程序的可見性、有序性、原子性。

換句話說,增刪改查之類的業務,差很少就得了,你得把時間精力放在重要業務上。好比說,銀行的轉帳、提現業務,電商的下單減庫存業務等等。

看到這兒,你可能會這麼想:我已經知道了併發問題的根源,也有了解決方案,也知道要特別注意某些業務,可我仍是很懵,徹底不知道該從哪裏開始。

不要緊,我再給你兩個抓手。

在數據競爭的場景下,必須考慮併發

數據競爭就是,多個線程同時訪問、並修改一個共享數據,就會致使併發BUG。這裏有兩個關鍵詞:多線程、修改一個共享數據,你看下面的代碼。

class Account {
    // 餘額
    private Integer balance = 1000;

    // 充值
    void charge(Integer amt) {
        this.balance += amt;
    }
}

上面是一段充值的代碼,若是充值一筆一筆地進行,這徹底沒問題。由於兩個條件沒湊齊,balance-餘額雖然是共享變量,但一天也沒幾筆充值進來,更別提有人同時充值了。

可公司總有作大的一天,到時若是同時進來幾萬筆充值,那兩個條件就湊齊了,最後的餘額確定一塌糊塗。咱們來具體分析一下,這段代碼會同時出現可見性、原子性問題。

先來講可見性問題,如今是多核CPU時代,每顆核心都有本身的CPU緩存。若是一筆充值運行在CPU-1上,另外一筆充值運行在CPU-2上。這就至關於,它們同時讀取了 balance-餘額,又同時修改了 balance,但雙方沒有任何溝通,徹底不知道對方作了什麼,最後 balance 確定錯得一塌糊塗。

可見性問題

再來看原子性問題,Java是一門高級編程語言,一條語句每每會被拆成多個 CPU 指令。好比說,第八行代碼 this.balance -= amt; 就被拆成:

  1. 讀取內存,把 balance 加載到 CPU;
  2. CPU 執行 balance - amt 操做;
  3. 把最終的 balance 寫入內存;

這原本是一個完整的過程,可計算機有一個線程切換的機制,一旦發生了線程切換,那結果也就無法保證了。

原子性問題

關於可見性、原子性問題,能夠看下之前的文章:Java併發編程-併發根源,裏面有更詳細的分析。

總結一下,當多個線程同時訪問、並修改一個共享數據,會致使數據競爭,從而出現併發BUG。而數據競爭要知足兩個條件:多線程、修改一個共享數據,只要籌齊這兩個條件,你就得采起防禦措施了。

至於要採起什麼防禦措施,能夠參考這篇文章:Java併發編程-解決併發

有競態條件的代碼,必需要實現互斥

所謂競態條件,是指程序的執行結果,會隨着線程的執行順序而變化。這聽起來有點拗口,咱們仍是來看一個實際的例子吧。

在提現操做中,有一個條件判斷:提現金額不能大於帳戶餘額。但若是同時出現好幾筆提現,又沒作任何預防措施,就會出現超額提現的問題。

class Account {
    // 餘額
    private Integer balance = 150;

    // 提現
    void withdraw(Integer amt) {
        if (balance >= amt) {
            this.balance -= amt;
        }
    }
}

比方說,帳戶A 只有 150 塊,但線程1、線程二都要提現 100 塊。那正常來講,只有一筆轉帳能成功。

可若是線程1、線程二同時執行到第 7 行 if (balance >= amt),它們都發現提現金額是 100 塊,小於帳戶餘額 150 塊,因而兩筆提現都繼續執行,你白白虧了 50 塊嗎?

看到這兒,相信你大概能明白什麼是競態條件。簡單來講,你得特別留意這樣的代碼:

if (狀態變量 知足 執行條件) {
    狀態變量 = new 狀態變量
}

並且,競態條件很是特殊,無法作簡單的歸類。它既不是原子性問題,也不是可見性問題,更不是有序性問題,純粹是由於程序不支持併發訪問,必須一個個排隊處理。

既然這樣,咱們只能採用互斥這種方案了,具體來講,就是:鎖。你能夠回顧一下這兩篇文章:Java併發編程-解決併發Java併發編程-用鎖的正確姿式

寫在最後

併發編程是一個打怪升級的過程,裏面有 3 個關卡:安全性問題、活躍性問題、性能問題。

第一關就是安全性問題,是指程序有沒有按照咱們的期待執行。

第一關其實不難,咱們只要注意:程序會不會出現數據競爭、競態條件,而後作好防禦措施就行,你能夠回顧一下這些文章:Java併發編程-解決併發Java併發編程-用鎖的正確姿式

相關文章
相關標籤/搜索