Java併發 -- 安全性、活躍性、效能
- 執行緒安全的本質是正確性 ,而正確性的含義是程式按照預期執行
- 理論上執行緒安全 的程式,應該要避免出現可見性問題(CPU快取)、原子性問題(執行緒切換)和有序性問題(編譯優化)
- 需要分析是否存線上程安全問題的場景: 存在共享資料且資料會發生變化,即有多個執行緒會同時讀寫同一個資料
- 針對該理論的解決方案:不共享資料,採用執行緒本地儲存 (Thread Local Storage,TLS);不變模式
資料競爭
資料競爭(Data Race ):多個執行緒同時訪問 同一資料,並且至少有一個 執行緒會寫這個資料
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; } }
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 ):程式的執行結果依賴於 執行緒執行的順序
-
在併發環境裡,執行緒的執行順序是不確定的
- 如果程式存在竟態條件 問題,那麼意味著程式的執行結果是不確定 的
轉賬
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 = \frac{1}{(1-p)+\frac{p}{n}}$,n為CPU核數,p為並行百分比,(1-p)為序列百分比
- 假如p=95%,n無窮大,加速比S的極限為20,即無論採用什麼技術,最高只能提高20倍的效能
解決方案
-
無鎖演算法和資料結構
- 執行緒本地儲存(Thread Local Storage,TLS)
- 寫入時複製(Copy-on-write)
- 樂觀鎖
- JUC中的原子類
- Disruptor(無鎖的記憶體佇列)
-
減少鎖持有的時間
,互斥鎖的本質是將並行的程式序列化,要增加並行度,一定要減少持有鎖的時間
- 使用細粒度鎖 ,例如JUC中的ConcurrentHashMap(分段鎖)
- 使用讀寫鎖 ,即讀是無鎖的,只有寫才會互斥的
效能指標
- 吞吐量 :在單位時間 內能處理的請求數量,吞吐量越高,說明效能越好
- 延遲 :從發出請求到收到響應的時間,延遲越小,說明效能越好
- 併發量 :能同時 處理的請求數量,一般來說隨著併發量的增加,延遲也會增加,所以延遲一般是基於併發量來說的
轉載請註明出處:http://zhongmingmao.me/2019/04/23/java-concurrent-safety-active-performance/
訪問原文「Java併發 -- 安全性、活躍性、效能 」獲取最佳閱讀體驗並參與討論