淺談併發的資料競爭(可見性)與競態條件(原子性)

vipshop_fin_dev發表於2018-09-23

其實併發比較根源的原因就是操作的原子性和狀態的可見性無法保證

個人理解:
    1. 資料競爭?指的是併發條件下,狀態屬性資訊不同步,產生讀寫誤差。之所以會有誤差,請參考JVM執行緒和堆的變數副本問題(快取)。
    2. 競態條件?指的是併發複合操作,結果不可預測問題。兩者容易混淆
資料競爭,競態條件.png


/**
 * Created by mori.wang on 2018/7/30.
 * 經典資料競爭:成員變數不用volatile 關鍵字,無法保證成員最終狀態能被正確讀到
 * */
public class DataRace {
	private long count;
	private volatile long countSafe;
	public void set(long newCount){
		count=newCount;
	}
	public void setSafe(long newCount){
		countSafe=newCount;
	}

	public long get(){
		return count;//
		/**
		 * A thread:dataRace.set
		 * B thread:dataRace.get
		 * 			此時出現資料競爭
		 * 			執行緒內各自有快取資料 沒同步到共享記憶體
		 * 			本應獲得A執行緒更新後的值 卻依舊獲得舊的值
		 * 			此時方法語義不明確,可能造成執行緒不安全
		 * 			不一定會出現競態條件,看執行緒執行順序和運氣以及呼叫語義*
		 */
	}

	public long getCountSafe(){

		return countSafe;
		/**
		 * 
		 * 保證countSafe線上程間可見
		 *
		 */
	}
}
package concurrentExample.cas;
import java.util.concurrent.atomic.AtomicLong;

/**
 * Created by mori.wang on 2018/7/30.
 * 經典競態條件:複合操作 自增
 * */
public class RaceCondition {
	private Long count;
	private AtomicLong countSafe;
	private void increase(){
		/**
		 * get count
		 * set count+1
		 * 複合操作
		 * 既存在資料競爭 get的時候可能另外一個set了
		 * 也存在競態條件 操作結果不可預測(針對該方法
		 */
		count++;
	}
	private void increaseSafe(){
		/**
		 * 安全可靠 cas 
		 */
		countSafe.addAndGet(1);

	}
}

典型併發問題:先檢查後執行

——摘自JAVA併發程式設計實戰
在實際情況中經常會遇到競態條件*例如,假定你計劃中午在University Avenue的星巴克與一位朋友會面。但當你到達那裡時,發現在University Avenue上有兩家星巴克,並且你不知道是哪一家。在12:00時,你沒有在星巴克A看到朋友,那麼就會去星巴B看看他是否在那裡但他也不在那裡。這有幾種可能:你的朋友遲到了,還沒到仵何一家星巴克;你的朋友在你離開後到星巴克A ; 你的朋友在星巴克B,但他去星巴克A找你,並且 此時正在去星巴克A的途中。假設是最糟糕的情況,即最後一種可能.現在是你們兩個都去過了兩家星巴究,且開始懷疑對方是否失約了.現在你會怎麼做? 到另一家 星巴克?來來回回要走名少次?除非你們之間約定了其種協議,否則你們整天都在University Avenue上走來走去,倍感沮喪。在“我去看看他是否在另一家星巴克”這種方法中,問題在於:當你在街上走時,你的朋 友可能已經離開了你要去的星巴克。你首先看看星巴克A,發現“他不在”,並且開始去找他。你可以在星巴克B屮做同樣的選擇,但不是同時發生。兩家星巴克之間有幾分鐘的路程, 而就在這幾分鐘的時間裡,系統的狀態可能會發生變化。
在星巴克這個示例中說明了一種競態條件.因為要獲得正確的結果(與朋友會面),必須 取決於亊件的發生時序(當你們到達星巴克時,在離開並去另一家星巴克之前會等待多長時 間……)。當你邁出前門時,你在星巴克A的觀察結果將變得無效,你的朋友可能從後門進來 了,而你卻不知道。這種觀察結果的失效就是大多數競態條件的本質一於一種可能失效的 觀察結果來做出判斷或者執行某個計算。這種型別的競態條件稱為“先檢査後執行”:首先觀 察到某個條件為真(例如檔案X不存在)•然後根據這個觀察結果採用相應的動作(建立檔案 X),但事實上,在你觀察到這個結果以及開始建立檔案之間,觀察結果可能變得無效(另一 個執行緒在這期間建立了檔案X),從而導致各種問題(未預期的異常、資料被覆蓋、檔案被破壞等)。
######引出實際遇到的坑:存在狀態的資料競爭,也存在併發下的競態條件

  1. 歷史坑
     ExecutorService es = Executors.newFixedThreadPool(4);無法保證FIFO導致交叉執行產生併發問題

偷偷吐槽下 看程式碼還是比較花時間的。雖然解決起來不難,復現溯源難。解決方案:改用FIFO的執行緒池或者去掉定時任務1(感覺這個任務1很雞肋,刪行不刪頭,而且任務23都會覆蓋資料的,不需要專門做個任務去刪)

  1. 自造坑:
    兩臺機器同時進行操作,恰好同時檢查資料庫中不存在記錄,都走入插入資料的流程

解決方案:
利用redis實現個分散式鎖

######END
理解了基本併發問題所在,可以嘗試接著瞭解解決併發問題同時帶來的效能損耗問題。瞭解更多併發的奧祕。一起看書吧~部落格的知識點零零碎碎,書本才能帶來系統化結構化的知識體系。(鎖的實現,效能的損耗,分散式問題,JDK5併發包的奧祕)

——By FCS小年輕

相關文章