一. 安全性問題 線程安全的本質是正確性,而正確性的含義是程式按照預期執行 理論上線程安全的程式,應該要避免出現可見性問題(CPU緩存)、原子性問題(線程切換)和有序性問題(編譯優化) 需要分析是否存線上程安全問題的場景:存在共用數據且數據會發生變化,即有多個線程會同時讀寫同一個數據 針對該理論的解 ...
一. 安全性問題
-
線程安全的本質是正確性,而正確性的含義是程式按照預期執行
-
理論上線程安全的程式,應該要避免出現可見性問題(CPU緩存)、原子性問題(線程切換)和有序性問題(編譯優化)
-
需要分析是否存線上程安全問題的場景:存在共用數據且數據會發生變化,即有多個線程會同時讀寫同一個數據
-
針對該理論的解決方案:不共用數據,採用線程本地存儲(Thread Local Storage,TLS);不變模式
Ⅰ. 數據競爭
數據競爭(Data Race):多個線程同時訪問同一數據,並且至少有一個線程會寫這個數據
1. add
private static final int MAX_COUNT = 1_000_000; private long count = 0; // 非線程安全 public void add() { int index = 0; while (++index < MAX_COUNT) { count += 1; } }
2. add + synchronized
private static final int MAX_COUNT = 1_000_000; private long count = 0; public synchronized long getCount() { return count; } public synchronized void setCount(long count) { this.count = count; } // 非線程安全 public void add() { int index = 0; while (++index < MAX_COUNT) { setCount(getCount() + 1); } }
- 假設count=0,當兩個線程同時執行getCount(),都會返回0
- 兩個線程執行getCount()+1,結果都是1,最終寫入記憶體是1,不符合預期,這種情況為竟態條件
Ⅱ. 竟態條件
- 竟態條件(Race Condition):程式的執行結果依賴於線程執行的順序
- 在併發環境里,線程的執行順序是不確定的
- 如果程式存在竟態條件問題,那麼意味著程式的執行結果是不確定的
1. 轉賬
public class Account { private int balance; // 非線程安全,存在竟態條件,可能會超額轉出 public void transfer(Account target, int amt) { if (balance > amt) { balance -= amt; target.balance += amt; } } }
Ⅲ. 解決方案
面對數據競爭和竟態條件問題,可以通過互斥的方案來實現線程安全,互斥的方案可以統一歸為鎖
二. 活躍性問題
活躍性問題:某個操作無法執行下去,包括三種情況:死鎖、活鎖、饑餓
Ⅰ. 死鎖
- 發生死鎖後線程會相互等待,表現為線程永久阻塞
- 解決死鎖問題的方法是規避死鎖(破壞發生死鎖的條件之一)
- 互斥:不可破壞,鎖定目的就是為了互斥
- 占有且等待:一次性申請所有需要的資源
- 不可搶占:當線程持有資源A,並嘗試持有資源B時失敗,線程主動釋放資源A
- 迴圈等待:將資源編號排序,線程申請資源時按遞增(或遞減)的順序申請
Ⅱ. 活鎖
- 活鎖:線程並沒有發生阻塞,但由於相互謙讓,而導致執行不下去
- 解決方案:在謙讓時,嘗試等待一個隨機時間(分散式一致演算法Raft也有採用)
Ⅲ. 饑餓
- 饑餓:線程因無法訪問所需資源而無法執行下去
- 線程的優先順序是不相同的,在CPU繁忙的情況下,優先順序低的線程得到執行的機會很少,可能發生線程饑餓
- 持有鎖的線程,如果執行的時間過長(持有的資源不釋放),也有可能導致饑餓問題
- 解決方案
- 保證資源充足
- 公平地分配資源(公平鎖) – 比較可行
- 避免持有鎖的線程長時間執行
三. 性能問題
- 鎖的過度使用可能會導致串列化的範圍過大,這會影響多線程優勢的發揮(併發程式的目的就是為了提升性能)
- 儘量減少串列,假設串列百分比為5%,那麼多核多線程相對於單核單線程的提升公式(Amdahl定律)
S=1/((1-p)+p/n),n為CPU核數,p為並行百分比,(1-p)為串列百分比
- 假如p=95%,n無窮大,加速比S的極限為20,即無論採用什麼技術,最高只能提高20倍的性能
Ⅰ. 解決方案
- 無鎖演算法和數據結構
- 線程本地存儲(Thread Local Storage,TLS)
- 寫入時複製(Copy-on-write)
- 樂觀鎖
- JUC中的原子類
- Disruptor(無鎖的記憶體隊列)
- 減少鎖持有的時間,互斥鎖的本質是將並行的程式串列化,要增加並行度,一定要減少持有鎖的時間
- 使用細粒度鎖,例如JUC中的ConcurrentHashMap(分段鎖)
- 使用讀寫鎖,即讀是無鎖的,只有寫才會互斥的
Ⅱ. 性能指標
- 吞吐量:在單位時間內能處理的請求數量,吞吐量越高,說明性能越好
- 延遲:從發出請求到收到響應的時間,延遲越小,說明性能越好
- 併發量:能同時處理的請求數量,一般來說隨著併發量的增加,延遲也會增加,所以延遲一般是基於併發量來說的
寫在最後