併發與多執行緒之執行緒安全篇

追風少年瀟歌發表於2022-01-04

併發與多執行緒之執行緒安全篇

​ 併發是指某個時間段內,多個任務交替執行的能力。 CPU 把可執行時間均勻地分成若干份,每個程式執行一段時間後,記錄當前的工作狀態,釋放當前的執行資源並進入等待狀態,讓其他程式搶佔 CPU 資源。並行是指同時處理多工的能力。目前, CPU 已經發展為多核,可以同時執行多個互不依賴的指令及執行塊。

​ 併發和並行的目標都是儘可能快執行完所有的任務,兩者區別核心在於程式是否同時執行,併發環境有著以下幾個特點:

  1. 併發程式之間有相互制約的關係。
  2. 併發程式的執行過程是斷斷續續的。
  3. 當併發設定合理並且 CPU 擁有足夠的處理能力時,併發會提高程式的執行效率。

執行緒安全

​ 我們都知道,程式是作業系統進行資源分配的獨立單位,而執行緒是CPU排程和分派的基本單位,為了更充分地利用CPU資源,一般都會使用多執行緒進行處理。多執行緒的作用是提高任務的平均執行速度

​ 執行緒可以擁有自己的操作棧、程式計數器、區域性變數表等資源,它與同一程式內的其他執行緒共享該程式的所有資源。執行緒在生命週期記憶體在多種狀態,分別為NEW(新建狀態)RUNNABLE(就緒狀態)RUNNING(執行狀態)BLOCKED(堵塞狀態)DEAD(死亡狀態) 五種狀態。執行緒狀態圖如下

執行緒狀態圖

NEW-新建狀態

​ New 是執行緒被建立且未啟動的狀態。執行緒被建立的方式有三種:第一種繼承 Thread 類,第二種實現 Runnable 介面,第三種實現 Callable 介面。由於 Java 單繼承限制,所以推薦第二種,實現 Runnable 介面可以使程式設計更加靈活,對外暴露的細節比較少,開發者只需專注於具體的實現,即 run 方法的實現。第三種方式是實現 Callable 介面,程式碼如下:

@FunctionalInterface
public interface Callable<V> {
	
    //	Computes a result, or throws an exception if unable to do so.
    //	Returns:computed result
    //	Throws:Exception – if unable to compute a result
    V call() throws Exception;
}

從註釋中看出,當出現無法計算的結果就會丟擲異常,並且Callable介面是存在返回值的。這是 Callable 跟 Runnable 的本質區別。

RUNNABLE-就緒狀態

​ Runnable 是呼叫 start() 之後並且在執行之前的狀態。執行緒的 start 方法不能多次呼叫,否則將丟擲 IllegalThreadStateException 異常。

RUNNING-執行狀態

​ Running 是 run() 正在執行時執行緒的狀態。執行緒可能會由於某些原因而退出 RUNNING ,如時間、異常、鎖、排程等。

BLOCKED-堵塞狀態

Blocked 是執行緒已經發生堵塞的狀態,具體發生堵塞的有以下幾種情況:

  • 同步堵塞:鎖被其他執行緒佔用。
  • 主動堵塞:呼叫 Thread 的某些方法,主動讓出 CPU 執行權,比如 sleep()、join() 等。
  • 等待堵塞:執行了wait() 。

DEAD-死亡狀態

Dead 是執行緒已經執行完 run 方法,或因異常錯誤導致退出的狀態,該狀態無法逆轉,即無法回到就緒狀態。


​ 在計算機的執行緒處理過程當中,因為每個執行緒輪流佔用 CPU 的計算資源,可能會出現某個執行緒尚未執行完就不得不中斷的情況,容易導致執行緒不安全。例如,在服務端某個高併發業務共享某使用者資料,首先 A 執行緒執行使用者的查詢任務,但是查詢出來的資料還未返回就退出 CPU 時間片;然後後面進來的 B 執行緒搶佔了 CPU 資源並覆蓋了該使用者資料,最後 A 執行緒重新執行,將 B 執行緒修改過後的資料返回給前端,導致頁面出現資料異常。所以,為了保證執行緒安全,在多個執行緒併發地競爭共享資源時,通常採用同步機制協調各個執行緒的執行,確保得到正確的結果。

執行緒安全問題只在多執行緒環境下出現,單執行緒序列執行並不會存在此問題。為了保證高併發場景下的執行緒安全,可以從以下四個維度來探討:

(1)資料單執行緒內可見。單執行緒總是安全的。通過限制資料僅在單執行緒內可見,可以避免資料被其他執行緒篡改。最典型的就是執行緒區域性變數,它儲存在獨立虛擬機器棧幀的區域性變數表中,與其他執行緒毫無瓜葛。ThreadLocal 就是採用這種方式來實現執行緒安全的。

(2)只讀物件。只讀物件總是安全的。它的特性是允許複製、拒絕寫入。最典型的只讀物件有 String、Integer 等。一個物件想要拒絕任何寫入,必須要滿足以下條件:使用 final 關鍵字修飾類,避免被繼承;使用 private final 關鍵字避免屬性被中途修改;沒有任何更新方法;返回值不能為可變物件。

(3)執行緒安全類。某些執行緒安全類的內部有非常明確的執行緒安全機制。比如 StringBuffer 就是一個執行緒安全類,它採用 synchronized 關鍵字來修飾相關方法。

(4)同步與鎖機制。如果想要對某個物件進行併發更新操作,但又不屬於上述三類,需要開發者在程式碼中自定義實現相關的安全同步機制。

執行緒安全的核心理念就是“要不只讀,要不加鎖”。JDK 提供的併發包,主要分成以下幾個類族:

(1)執行緒同步類。這些類使執行緒間的協調更加容易,支援了更加豐富的執行緒協調場景,逐步淘汰了使用 Object 的 wait() 和 notify() 進行同步的方式。主要代表有 CountDownLatch、Semaphore、CyclicBarrier 等。

(2)併發集合類。集合併發操作的要求是執行速度快,提取資料準。最典型的莫過於 ConcurrentHashMap ,經過不斷的優化,有剛開始的分段式鎖到後來的 CAS ,不斷的提高併發效能。除此之外,還有 ConcurrentSkipListMap 、 CopyOnWriteArrayList 、BlockingQueue 等。

(3)執行緒管理類。雖然 Thread 和 ThreadLocal 在 JDK1.0 就已經引入,但是真正把 Thread 的作用發揮到極致的是執行緒池。根據實際場景的需要,提供了多種建立執行緒池的快捷方式,如使用 Executors 靜態工廠或者使用 ThreadPoolExecutors 等。另外,通過 ScheduledExecutorService 來執行定時任務。

(4)鎖相關類。鎖以 Lock 介面為核心,派生出一些實際場景中進行互斥操作的鎖相關類。最有名的是 ReentrantLock 。

相關文章