01 | 可見性、原子性和有序性問題:併發編程Bug的源頭
併發程序幕後的故事
源頭之一:緩存導致的可見性問題
源頭之二:線程切換帶來的原子性問題
源頭之三:編譯優化帶來的有序性問題
總結
02 | Java內存模型:看Java如何解決可見性和有序性問題
什麼是Java內存模型?
使用volatile的困惑
Happens-Before 規則
前面一個操作的結果對後續操作是可見的
-
程序的順序性規則
這條規則是指在一個線程中,按照程序順序,前面的操作 Happens-Before 於後續的任意操作。 -
volatile變量規則
這條規則是指對一個volatile變量的寫操作, Happens-Before 於後續對這個volatile變量的讀操作。 -
傳遞性
這條規則是指如果A Happens-Before B,且B Happens-Before C,那麼A Happens-Before C。 -
管程中鎖的規則
這條規則是指對一個鎖的解鎖 Happens-Before 於後續對這個鎖的加鎖。 -
線程 start() 規則
這條是關於線程啓動的。它是指主線程A啓動子線程B後,子線程B能夠看到主線程在啓動子線程B前的操作。 -
線程 join() 規則
這條是關於線程等待的。它是指主線程A等待子線程B完成(主線程A通過調用子線程B的join()方法實現),當子線程B完成後(主線程A中join()方法返回),主線程能夠看到子線程的操作。當然所謂的“看到”,指的是對共享變量的操作。
總結
在Java語言裏面,Happens-Before的語義本質上是一種可見性,A Happens-Before B 意味着A事件對B事件來說是可見的,無論A事件和B事件是否發生在同一個線程裏。例如A事件發生在線程1上,B事件發生在線程2上,Happens-Before規則保證線程2上也能看到A事件的發生。
03 | 互斥鎖(上):解決原子性問題
那原子性問題到底該如何解決呢?
你已經知道,原子性問題的源頭是線程切換
“同一時刻只有一個線程執行”這個條件非常重要,我們稱之爲互斥。
簡易鎖模型
改進後的鎖模型
Java語言提供的鎖技術:synchronized
04 | 互斥鎖(下):如何用一把鎖保護多個資源?
保護沒有關聯關係的多個資源
保護有關聯關係的多個資源
總結
“原子性”的本質是什麼?其實不是不可分割,不可分割只是外在表現,其本質是多個資源間有一致性的要求,操作的中間狀態對外不可見。
05 | 一不小心就死鎖了,怎麼辦?
如何預防死鎖
- 破壞佔用且等待條件
- 破壞不可搶佔條件
- 破壞循環等待條件
06 | 用“等待-通知”機制優化循環等待
用synchronized實現等待-通知機制
07 | 安全性、活躍性以及性能問題
那是不是所有的代碼都需要認真分析一遍是否存在這三個問題呢?當然不是,其實只有一種情況需要:存在共享數據並且該數據會發生變化,通俗地講就是有多個線程會同時讀寫同一數據。
併發編程中我們需要注意的問題有很多,很慶幸前人已經幫我們總結過了,主要有三個方面,分別是:安全性問題、活躍性問題和性能問題。
安全性問題
當多個線程同時訪問同一數據,並且至少有一個線程會寫這個數據的時候,如果我們不採取防護措施,那麼就會導致併發Bug,對此還有一個專業的術語,叫做數據競爭(Data Race)
競態條件,指的是程序的執行結果依賴線程執行的順序。
面對數據競爭和競態條件問題,又該如何保證線程的安全性呢?其實這兩類問題,都可以用互斥這個技術方案,而實現互斥的方案有很多,CPU提供了相關的互斥指令,操作系統、編程語言也會提供相關的API。從邏輯上來看,我們可以統一歸爲:鎖。
活躍性問題
所謂活躍性問題,指的是某個操作無法執行下去。我們常見的“死鎖”就是一種典型的活躍性問題,當然除了死鎖外,還有兩種情況,分別是“活鎖”和“飢餓”。
有時線程雖然沒有發生阻塞,但仍然會存在執行不下去的情況,這就是所謂的“活鎖”。
所謂“飢餓”指的是線程因無法訪問所需資源而無法執行下去的情況。
性能問題
Java SDK併發包裏之所以有那麼多東西,有很大一部分原因就是要提升在某個特定領域的性能。
總結
08 | 管程:併發編程的萬能鑰匙
什麼是管程
MESA模型
wait()的正確姿勢
notify()何時可以使用
總結
09 | Java線程(上):Java線程的生命週期
通用的線程生命週期
Java中線程的生命週期
-
RUNNABLE與BLOCKED的狀態轉換
只有一種場景會觸發這種轉換,就是線程等待synchronized的隱式鎖 -
RUNNABLE與WAITING的狀態轉換
第一種場景,獲得synchronized隱式鎖的線程,調用無參數的Object.wait()方法
第二種場景,調用無參數的Thread.join()方法。
第三種場景,調用LockSupport.park()方法。調用LockSupport.park()方法,當前線程會阻塞,線程的狀態會從RUNNABLE轉換到WAITING。調用LockSupport.unpark(Thread thread)可喚醒目標線程,目標線程的狀態又會從WAITING狀態轉換到RUNNABLE。 -
RUNNABLE與TIMED_WAITING的狀態轉換
-
從NEW到RUNNABLE狀態,調用線程對象的start()方法
-
從RUNNABLE到TERMINATED狀態
那stop()和interrupt()方法的主要區別是什麼呢?
10 | Java線程(中):創建多少線程纔是合適的?
爲什麼要使用多線程?
度量性能的指標有很多,但是有兩個指標是最核心的,它們就是延遲和吞吐量。
多線程的應用場景
創建多少線程合適?
CPU密集型的計算場景,理論上“線程的數量=CPU核數”就是最合適的。不過在工程上,線程的數量一般會設置爲“CPU核數+1”
對於I/O密集型計算場景,最佳線程數=CPU核數 * [ 1 +(I/O耗時 / CPU耗時)]
其實只要把握住一條原則就可以了,這條原則就是將硬件的性能發揮到極致。
11 | Java線程(下):爲什麼局部變量是線程安全的?
方法是如何被執行的
局部變量存哪裏?
調用棧與線程
線程封閉
12 | 如何用面向對象思想寫好併發程序?
一、封裝共享變量
對於這些不會發生變化的共享變量,建議你用final關鍵字來修飾
二、識別共享變量間的約束條件
三、制定併發訪問策略
13 | 理論基礎模塊熱點問題答疑
那這個“串行的故事”是怎樣的呢?