## 10 有福同享,有難同當—原子性
> 耐心和恒心總會得到報酬的。
> ——愛因斯坦
從本節開始,我們進入新一章的學習–《并發的問題和原因詳解》。關于如何實現并發,前文已經做了詳盡的講解。現在我們可以輕松的啟動多個線程來完成工作,但是同樣也會面臨各種各樣的問題。例如前一節的例子,學生和老師都要訪問任務列表這個共享資源的時候,我們的程序必須加上同步才能正常運行。其實這只是問題之一,還有更多的問題等待著我們。
所有并發程序都需要保證線程的安全性,那么什么是線程的安全性呢?其實很難給出一個非常正式的定義。有些定義雖然沒有錯誤,但好像說的又是廢話。例如,線程安全是指一個類在多線程并發的情況下可以安全使用。這種定義沒有任何指導價值。
其實線程安全中的安全,是指程序的正確性。程序不但要在單線程的時候保證正確,在多線程并發的時候也要保證程序計算的正確性。比如我們最初幾版抄寫單詞的代碼,只有一個線程運行是正確的,但多線程并發會使得抄寫次數超過要求次數,也就是說程序運行結果不正確,那么就是非線程安全的。最后一版我們經過修改,確保多線程并行,抄寫次數的總和等于要求的次數,那么就是線程安全的。所以我們線程安全可以這樣定義:某個類,在多線程并發訪問時,始終能夠確保運行的正確性,那么這個類就是線程安全的。
確保線程安全,會面對諸多挑戰。在本章中,我們將分析多線程開發中會遇到的典型問題以及其產生的根本原因。解決了這些問題,也就保證了線程安全。最后為了幫助大家對多線程問題產生的原因有更為深入的理解,我會用一節來介紹Java的內存模型。只有深刻理解了我們所使用語言的底層原理,才能夠從容應對任何問題,萬變不離其宗。
## 1\. 并發編程的三大特性
所有講并發編程的書籍都會講到并發編程的三大特性,這是并發編程中所有問題的根源,我們只有深刻理解了這三大特性,才不會編寫出漏洞百出的并發程序,才不會遇到問題時無從下手,才不會對自己的程序沒有信心。
這三大特性是:
1、原子性
所有操作要么全部成功,要么全部失敗。
2、可見性
一個線程對變量進行了修改,另外一個線程能夠立刻讀取到此變量的最新值。
3、有序性
代碼在執行階段,并不一定和你的編寫順序一致。
以上是對三大特性的簡單解釋。不理解也沒有關系,本章中會一一進行講解。在本節中我們重點來看原子性。
## 2\. 什么是原子性
原子性是三大特性中最好理解的一個。只要你做過程序開發,應該都會聽說過原子性。如果沒有,那么至少聽說過事務吧?原子性是事務的四大特性—ACID 之一,并且位居首位,可見其重要性。那么到底什么是原子性呢?原子性的重點在原子上。如果你的初中物理和化學,還沒有因全身心投入到計算機行業,而全部還給老師,那么應該還記得原子在化學反應中不可以再分割。其實所謂的原子性就是不可分割性。做為一個整體的N次操作不可分割,一榮俱榮,一損俱損。
我們抄寫單詞的例子中有三步操作。第一步,查詢剩余抄寫次數。第二步,如果剩余次數大于零,把次數-1。第三步,把新的剩余次數更新到 punishment 對象中。這三步操作是原子操作。在操作期間,別的線程不能讀取剩余抄寫次數,以免別的取到更新前的舊值而重復抄寫。這里我們引入一個新的概念:競態條件。
## 3\. 競態條件
競態條件是指,在多線程的情況下,由于多個線程執行的時序不同,而出現不正確的結果。上文的例子是典型的先檢查后執行,這也是最常見的競態條件類型。上面例子的問題出現在第 2、3 步操作依賴于第1步的檢查,而第一步的檢查結果并不能保證在執行 2、3 步的時候依舊有效。這是因為其它線程可能在你在執行完第一步時已經改變了剩余次數。此時 2,3 步依舊會按照已經失效的檢查結果繼續執行,那么線程安全問題就出現了。
其實現實中,我們也會經常遇到競態條件。舉個例子,你的室友中午要出去辦事,可能趕不上下午第一節課。他拜托你,如果老師點名時他還沒回來,幫他答一下到。下午第一節課果然老師點名了,眼看就要點到你的室友,你環顧了下四周,確認室友沒有趕回來,然后緊張的等待老師點到室友的名字。老師又點了幾個名字后,終于點到了你室友的名字。你故作鎮定,沉穩、大方的喊了聲:到!但令人尷尬的是,幾乎同時,教室后面也傳出了一聲鏗鏘有力的到!你回頭一看,就這幾秒鐘的時間,室友已經趕回了教室,從后門溜進來坐在了最后一排。

這就是競態條件,你觀察到室友沒有來上課的結果,在你替室友答到的時候已經失效了。但你并不知道,依舊按照失效的觀測結果執行答到。最后造成了尷尬的局面。你們精心設計好的程序執行錯誤,穿幫了。這多像我們精心編寫一段并發程序,信心滿滿的去執行,卻發現執行結果是錯誤的。
競態條件并不一定會造成問題,正如我們前面的程序,在第一版改進后,抄寫 1000 次單詞,并不會出現錯誤。但是抄寫 1 萬次以上時,就會出現問題。這是因為在多線程執行時,不同線程的不同步驟在特定時序執行才會出問題。而執行次數越多就越可能碰上導致出錯的特定時序。回到例子,如果你的室友沒有偏偏趕在你觀察和點名之前那個時間段回到教室,就不會出現任何問題。
單例是個經典的話題。僅是單例有幾種寫法,都夠程序員們爭論幾天。其中有一種寫法如下:
~~~java
public class Singleton {
private static Singleton singleton = null;
private Singleton() {
}
public static Singleton getInstance() {
if(singleton==null){
singleton = new Singleton();
}
return singleton;
}
}
~~~
這段代碼在非并發的情況下沒有任何問題。但是在并發的情況下,因為競態條件有可能引發錯誤。如果線程 A 在判斷 singleton 為空并且創建 singleton 對象之前,線程B也開始執行這段代碼,它同樣會判斷 singleton 為空去創建 singleton,這樣本來的單例卻變成了雙例,和我們期望的正確結果不一致。
## 3\. 總結
如果在需要保證原子性的一組操作中,有競態條件產生,那么就會出現線程安全的問題。我們可以通過為原子操作加鎖或者使用原子變量來解決。原子變量在 java.util.concurrent.atomic 包中,它提供了一系列的原子操作。后面的章節我們會深入講解原子變量的使用和原理。
- 前言
- 第1章 Java并發簡介
- 01 開篇詞:多線程為什么是你必需要掌握的知識
- 02 絕對不僅僅是為了面試—我們為什么需要學習多線程
- 03 多線程開發如此簡單—Java中如何編寫多線程程序
- 04 人多力量未必大—并發可能會遇到的問題
- 第2章 Java中如何編寫多線程
- 05 看若兄弟,實如父子—Thread和Runnable詳解
- 06 線程什么時候開始真正執行?—線程的狀態詳解
- 07 深入Thread類—線程API精講
- 08 集體協作,什么最重要?溝通!—線程的等待和通知
- 09 使用多線程實現分工、解耦、緩沖—生產者、消費者實戰
- 第3章 并發的問題和原因詳解
- 10 有福同享,有難同當—原子性
- 11 眼見不實—可見性
- 12 什么?還有這種操作!—有序性
- 13 問題的根源—Java內存模型簡介
- 14 僵持不下—死鎖詳解
- 第4章 如何解決并發問題
- 15 原子性輕量級實現—深入理解Atomic與CAS
- 16 讓你眼見為實—volatile詳解
- 17 資源有限,請排隊等候—Synchronized使用、原理及缺陷
- 18 線程作用域內共享變量—深入解析ThreadLocal
- 第5章 線程池
- 19 自己動手豐衣足食—簡單線程池實現
- 20 其實不用造輪子—Executor框架詳解
- 第6章 主要并發工具類
- 21 更高級的鎖—深入解析Lock
- 22 到底哪把鎖更適合你?—synchronized與ReentrantLock對比
- 23 按需上鎖—ReadWriteLock詳解
- 24 經典并發容器,多線程面試必備—深入解析ConcurrentHashMap上
- 25 經典并發容器,多線程面試必備—深入解析ConcurrentHashMap下
- 26不讓我進門,我就在門口一直等!—BlockingQueue和ArrayBlockingQueue
- 27 倒數計時開始,三、二、一—CountDownLatch詳解
- 28 人齊了,一起行動—CyclicBarrier詳解
- 29 一手交錢,一手交貨—Exchanger詳解
- 30 限量供應,不好意思您來晚了—Semaphore詳解
- 第7章 高級并發工具類及并發設計模式
- 31 憑票取餐—Future模式詳解
- 32 請按到場順序發言—Completion Service詳解
- 33 分階段執行你的任務-學習使用Phaser運行多階段任務
- 34 誰都不能偷懶-通過 CompletableFuture 組裝你的異步計算單元
- 35拆分你的任務—學習使用Fork/Join框架
- 36 為多線程們安排一位經理—Master/Slave模式詳解
- 第8章 總結
- 37 結束語