經過前面的 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;
就被拆成:
這原本是一個完整的過程,可計算機有一個線程切換的機制,一旦發生了線程切換,那結果也就無法保證了。
關於可見性、原子性問題,能夠看下之前的文章: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併發編程-用鎖的正確姿式