併發王者課-青銅03:興利除弊-如何理解多執行緒帶來的安全問題
歡迎來到《併發王者課》,本文是該系列文章中的第3篇。
在前面的兩篇文章中,我們體驗了執行緒的建立,並從OS程式層面認識了執行緒。現在,我們已經知曉多執行緒在解決一些場景問題時有特效。
然而,不知你可曾想過,多執行緒雖然效率很高,但是它卻有著你無法迴避的併發問題。舉個王者中常見的場景,雙方10人同時進攻主宰,最後擊敗主宰的玩家才是真正的贏家,而且只能有一位。所以問題來了,假如這10位玩家代表10個執行緒,它們在併發訪問同一個資源時,如何保證資料的安全性?總不至於,主宰只有一條命,可是卻有多位玩家獲得主宰,這顯然不符合邏輯。
這個簡單例子的背後,是計算機系統中一個普遍且基本的問題,即多執行緒的安全問題。在設計多執行緒時,我們追求它的優點,也務必要理解它存在的安全隱患,併為之設計合理的解決方案。否則,多執行緒這把雙刃劍必將給我們以教訓。
本文將從併發與並行的概念觸發,幫助你入門這些概念並理解競態相關問題。
一、理解併發(Concurrency)和並行(Parallelism)
在併發程式設計中,併發與並行像一對孿生兄弟,不僅長相相似,又容易讓人混淆。但是,它們又有著本質的區別。所以,理解並行與併發,不要嘗試去死記硬背概念,在你未能從本質上認識它們之前,你無法欺騙你的大腦去記住它。
簡而言之,並行與併發的區別的核心在於所競爭的 資源 不同。舉個通俗的例子:
- 藍方5個人一起打主宰,是併發(Concurrency),因為競爭的目標資源只有一個;
- 藍方2人去打主宰,3人去打暴君,是並行(Parallelism),因為競爭的目標資源是兩個。
類似的,從CPU計算的角度看, 併發和並行的概念可以理解為:
- 如果1個CPU同時執行5個任務,就是併發;
- 如果5個CPU同時執行5個任務,並且是每個CPU執行一個,那麼就是並行。
以上是對並行和併發的通俗概述,如果你有興趣,可以透過檢索資料詳細瞭解單CPU下是如何模擬併發的。
二、理解競態(Race Condition)下的安全問題
顯然易見,無論是併發還是並行,都有助於提高計算效率。然而,效率是一方面,安全則是更重要的一方面。比如上面進攻主宰的案例中,一定要能知道是誰給予了最後一擊,也就是資料不能出錯。所以,我們就需要理解多執行緒下的 競態(Race Condition) 和解決策略。
所謂競態,你可以理解為多個執行緒試圖在同一時刻修改共享資料的情況。你看,從字面上理解的話,Race這個詞就是比賽的意思。比賽的目標是什麼?是看誰先獲得共享資源,即進入臨界區(Critical Section)。
常見的競態有下面這兩種模式:
- Read-modify-write
- Check-then-act
1. Read-modify-write
先看下面這段程式碼,玩家每次進攻,主宰的血量都會減少:
public class Master {
//主宰的初始血量
private int blood = 100;
//每次被擊打後血量減5
public int decreaseBlood() throws Exception {
if(blood 0){
throw new Exception("主宰已經被擊敗!");
}
blood = blood - 5;
return blood;
}
}
當執行緒執行decreaseBlood()
方法呼叫時,事情是這樣發展的:
- 第一步:從記憶體中讀取
blood
的值到暫存器(Read); - 第二步:修改暫存器中的
blood
值(Modify); - 第三步:將暫存器的值寫回記憶體(Write)。
這就是Read-modify-write模式。整個過程看起來一氣呵成,實則禍根已經種下。想想看,如果在第一步時,兩個執行緒同時都讀取到了值(比如100),隨後兩個執行緒同時做了修改,此時在第三步,無論是哪個執行緒率先將值寫回記憶體,後面的執行緒都會覆蓋記憶體中的值。換句話說,主宰承受了兩次攻擊,血量應該降低到90,可結果卻是95,不是它耐操,而是你程式碼寫錯了!
2. Check-then-act
//每次被擊打後血量減5
public int decreaseBlood() throws Exception {
if(blood 0){
throw new Exception("主宰已經被擊敗!");
}
blood = blood - 5;
return blood;
}
我們再近距離觀察下decreaseBlood()
方法,你會發現,它不僅會讓主宰出現攻擊兩次但血量卻只減少一次的情況,還會出現血量為負值的情況!這是為什麼?
注意decreaseBlood()
中有一行if(blood ,也就是說如果此時主宰已經被擊敗,那就不要再往下繼續執行,直接丟擲異常。但是,問題來了。假設此時主宰的血量是 5 ,就差最後一擊了!然後,執行緒A和執行緒B兩個執行緒同時進來:
- 第一步:執行緒A和執行緒B檢查血量是否為 0 (Check);
- 第二步:執行緒A和執行緒B都透過了檢查;
- 第三步:執行緒A和執行緒B執行血量扣減動作,但順序未知(Act)。
問題是,如果執行緒A在執行blood = blood - 5
時,blood
的值不再是 5 ,而是已經被執行緒B更改為 0 了呢?那麼結果就是主宰最後的血量是 -5 !很顯然,這樣的結果就扯淡了。
以上就是兩種常見的競態情況。簡單來說, Read-modify-write是在寫入時因併發導致值被覆蓋,而Check-then-act則是因併發導致條件判斷失效。
3. 如何預發競態
既然多執行緒是不安全的,那如何預防競態的發生?其核心在於鎖+原子操作,即對臨界區進行加鎖,讓臨界區每次有且只能有一個執行緒訪問,在當前執行緒未離開臨界區時,其他執行緒不得進入,且執行緒在臨界區的操作必須保證原子性。
在Java中,最簡單的加鎖方式是使用synchronized
關鍵字,我們會在下一篇中對它詳細講解。
以上就是文字的全部內容,恭喜你又上了一顆星✨
夫子的試煉
- 寫一段多執行緒併發程式碼,體驗併發時的資料錯誤。
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/4822/viewspace-2797133/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- 併發王者課-青銅9:防患未然-如何處理執行緒中的異常執行緒
- 多執行緒併發安全問題詳解執行緒
- 併發王者課-青銅8:分工協作-從本質認知執行緒的狀態和動作方法執行緒
- 併發王者課-青銅5:一探究竟-如何從synchronized理解Java物件頭中的鎖synchronizedJava物件
- 03 執行緒安全問題執行緒
- 如何解決多執行緒併發問題執行緒
- 併發王者課-青銅07:順藤摸瓜-如何從synchronized中的鎖認synchronized
- HashMap多執行緒併發問題分析HashMap執行緒
- 併發王者課-青銅10:千錘百煉-如何解決生產者與消費者經典問題
- 併發王者課-青銅7:順藤摸瓜-如何從synchronized中的鎖認識Monitorsynchronized
- Java多執行緒和併發問題集Java執行緒
- 多執行緒與高併發(二)執行緒安全執行緒
- 併發與多執行緒之執行緒安全篇執行緒
- 多執行緒併發篇——如何停止執行緒執行緒
- 多執行緒,你覺得你安全了?(執行緒安全問題)執行緒
- Java多執行緒中執行緒安全與鎖問題Java執行緒
- 多執行緒併發同步問題及解決方案執行緒
- ThreadLocal執行緒重用時帶來的問題thread執行緒
- 多執行緒的安全性問題(三)執行緒
- Java併發專題(二)執行緒安全Java執行緒
- C#多執行緒開發-執行緒池03C#執行緒
- 啃碎併發(五):Java執行緒安全特性與問題Java執行緒
- 併發王者課-黃金2:行穩致遠-如何讓你的執行緒免於死鎖執行緒
- JAVA多執行緒併發Java執行緒
- 王者併發課-鉑金3:一勞永逸-如何理解鎖的多次可重入問題
- 併發王者課-鉑金3:一勞永逸-如何理解鎖的多次可重入問題
- 最常見的15個Java多執行緒,併發面試問題Java執行緒面試
- 深入理解Java多執行緒與併發框(第①篇)——執行緒的狀態Java執行緒
- 多執行緒的安全問題及解決方案執行緒
- 5分鐘搞懂多執行緒安全問題執行緒
- 多執行緒與高併發(一)多執行緒入門執行緒
- ArrayList 的執行緒安全問題執行緒
- 【python高併發】程序、執行緒的理解Python執行緒
- 併發王者課-鉑金9:互通有無-Exchanger如何完成執行緒間的資料交換執行緒
- React 從青銅到王者系列教程之倔強青銅篇React
- 併發程式設計之多執行緒執行緒安全程式設計執行緒
- Concurrency(一:如何理解多執行緒)執行緒
- 深入理解Java多執行緒與併發框(第⑪篇)——執行緒池引數Java執行緒