1. 程式人生 > >Java併發程式設計--執行緒安全問題與解決方案

Java併發程式設計--執行緒安全問題與解決方案

本文簡介:

用多執行緒開發的人都知道,在多執行緒的開發過程中有可能會出現執行緒安全問題(專業術語叫記憶體可見性問題),但並不一定每次都會出現。出現這樣的情況,也會另開發者頭皮發麻,無從下手,接下來我們會慢慢深入,揭開多執行緒的神祕面紗。

本文主要介紹了Java多執行緒開發的優勢,使用該技術可能會出現的一些記憶體不可見問題以及相應的解決措施。通過本文,讀者將學習到如下幾塊知識:

  1. 為什麼需要多執行緒技術(多執行緒的優勢)
  2. 使用多執行緒技術帶來的一些問題
  3. 產生記憶體不可見問題的條件
  4. 產生記憶體不可見問題的原因
  5. 我們該如何分析會不會產生記憶體可見問題及出現問題之後的解決方式

下面進入正文

為什麼需要多執行緒技術(多執行緒的優勢)

執行緒是Java語言中不可或缺的重要部分,它們能使複雜的非同步程式碼變得簡單,簡化複雜系統的開發;能充分發揮多處理器系統的強大計算能力。

  1. 充分利用硬體資源。由於執行緒是CPU的基本排程單位,所以如果是單執行緒,那麼最多隻能同時在一個處理器上執行,意味著其他的CPU資源都將被浪費。而多執行緒可以同時在多個處理器上執行,只要各個執行緒間的通訊設計正確,那麼多執行緒將能充分利用處理器的資源。
  2. 結構優雅。多執行緒程式能將程式碼量巨大,複雜的程式分成一個個簡單的功能模組,每塊實現複雜程式的一部分單一功能,這將會使得程式的建模,測試更加方便,結構更加清晰,更加優雅。
  3. 簡化非同步處理。為了避免阻塞,單執行緒應用程式必須使用非阻塞I/O,這樣的I/O複雜性遠遠高於同步I/O,並且容易出錯。

使用多執行緒技術帶來的一些問題

  1. 執行緒安全( 記憶體可見性問題):由於統一程序下的多個執行緒是共享同樣的地址空間和資料的,又由於執行緒執行順序的不可預知性,一個執行緒可能會修改其他執行緒正在使用的變數,這一方面是給資料共享帶來了便利;另一方面,如果處理不當,會產生髒讀,幻讀等問題,好在Java提供了一系列的同步機制來幫助解決這一問題,例如內建鎖。
  2. 活躍性問題。可能會發生長時間的等待鎖,甚至是死鎖。
  3. 效能問題。 執行緒的頻繁排程切換會浪費資源,同步機制會導致記憶體緩衝區的資料無效,以及增加同步流量。

產生記憶體不可見問題(執行緒安全問題)的條件

產生記憶體不可見的條件有倆個
1. 多個執行緒
2. 存在共享變數
當多個執行緒操作了共享變數時,就有可能會產生執行緒安全問題(記憶體不可見問題)。

產生記憶體不可見問題的原因

產生記憶體不可見的問題有三個原因
1. 沒有保證程式碼的原子性
2. 沒有保證程式碼的可見性
3. 沒有保證程式碼的有序性
1.1 沒有保證原子性產生記憶體不可見的究極原因 ?
由於現代的CPU是多核的,可以實現並行,所以讀書的時候我一直帶著這樣的疑問?
多核會不會同時操作同一記憶體下的資料:舉例 程式碼 i++;多核處理器會不會同時執行這行程式碼?答案是不會同時執行。
請參考
1.2 沒有保證程式碼的可見性產生記憶體不可見的究極原因?
可見性定義:一個執行緒的寫對另一個執行緒立即可知。
現代的處理器都有讀寫緩衝區,讀緩衝區非同步從主存中讀取資料,寫緩衝區臨時儲存向記憶體寫入的資料。有了寫緩衝區可以保證指令流水線持續進行,它可以避免由於處理器停頓下來等待向記憶體寫入資料而產生的延遲。同時以非同步的方式重新整理寫緩衝區,以及合併寫緩衝區中對同一個地址的多次寫,減少對記憶體匯流排的佔用。

假設執行緒A,B,分別在倆個處理器上執行,初始時a=0 ,執行緒A先執行 a=1, 執行緒B 再執行 a=2,執行緒3去讀變數a,最後a會是幾呢? 答案 0 1 2 都有可能。
產生0的原因:執行緒A先執行 a=1,然後放到寫緩衝區, 執行緒B 再執行 a=2,然後又放到寫緩衝區,假設倆個寫緩衝區還沒又重新整理的時候 執行緒3去讀變數a,此時執行緒3讀取到變數a就為0 。
產生1的原因:執行緒A先執行 a=1,然後放到寫緩衝區, 執行緒B 再執行 a=2,然後又放到寫緩衝區,假設執行緒A所在處理器的寫緩衝區已重新整理,執行緒B還沒又重新整理的時候, 執行緒3去讀變數a,此時執行緒3讀取到變數a就為1 。
產生2的原因:執行緒A先執行 a=1,然後放到寫緩衝區, 執行緒B 再執行 a=2,然後又放到寫緩衝區,假設執行緒B所在處理器的寫緩衝區已重新整理,執行緒A還沒又重新整理的時候, 執行緒3去讀變數a,此時執行緒3讀取到變數a就為2 。
綜上所訴,沒有保證程式碼的可見性產生記憶體不可見的究極原因是:寫緩衝區的存在,且以非同步的方式重新整理緩衝。
1.3 沒有保證程式碼的有序性產生記憶體不可見的究極原因?
編譯器和處理器為了優化程式的效能會進行從排序,所以有序性會產生記憶體不可見問題。

我們該如何分析會不會產生記憶體可見問題及出現問題之後的解決方式

如何分析我們的程式碼會不會產生記憶體可見性問題

Java虛擬機器提供了Java記憶體模型來分析會不會產生記憶體可見性問題,Java記憶體模型也提供了一系列規則(happens-before等等)來輔助程式設計師更好的分析會不會產生記憶體可見行問題。

出現問題之後的解決方式

產生執行緒安全問題的原因有三個,解決方式無非就是 1.保證程式碼的原子性 2.保證程式碼的可見性 3.保證程式碼的有序性。
JVM 提供了synchronized 和volatile 關鍵字來解決問執行緒安全問題
Synchronized 可以保證程式碼的原子性、保證程式碼的可見性、保證程式碼的有序性。
volatile 可以保證程式碼的可見性、保證程式碼的有序性但不能保證程式碼的原子性。
所以Synchronized可以完全解決執行緒安全問題,而volatile不可以。

總結

為什麼本篇要這麼寫呢?因為讀完Java併發程式設計書籍之後,我覺得非常的混亂,讀完之後感覺沒有收穫,就想寫點東西做個總結,可是又無從下手,不知道如何引入到Java記憶體模型和synchronized 、volatile 關鍵字。為什麼無從下手呢,後來發現是對Java記憶體模型和synchronized 、volatile 理解不夠到位。

Java記憶體模型可以幫助我們理解我們的程式碼會不會存在記憶體可見性問題。
synchronized 、volatile可以幫助我們解決記憶體可見性問題。前者是分析會不會產生問題,後者是產生問題之後的解決。

既然理清了倆者的區別,那麼該如何過渡到這呢,LZ想從本篇引入到併發學習中。