1. 程式人生 > >併發程式設計之執行緒基礎---01

併發程式設計之執行緒基礎---01

文章目錄

1.執行緒狀態

  執行緒一共有 6 種狀態(NEW、RUNNABLE、BLOCKED、WAITING、TIME_WAITING、TERMINATED)

  • NEW:初始狀態,執行緒被構建,但是還沒有呼叫 start 方法
  • RUNNABLED:執行狀態,JAVA 執行緒把作業系統中的就緒和執行兩種狀態統一 稱為“執行中”
  • BLOCKED:阻塞狀態,表示執行緒進入等待狀態,也就是執行緒因為某種原因放棄 了 CPU 使用權,阻塞也分為幾種情況:

   Ø等待阻塞:執行的執行緒執行 wait 方法,jvm 會把當前執行緒放入到等待佇列
  Ø同步阻塞:執行的執行緒在獲取物件的同步鎖時,若該同步鎖被其他執行緒鎖佔 用了,那麼 jvm 會把當前的執行緒放入到鎖池中
  Ø其他阻塞:執行的執行緒執行 Thread.sleep 或者 t.join 方法,或者發出了 I/O 請求時,JVM
會把當前執行緒設定為阻塞狀態,當 sleep 結束、join 執行緒終止、 io 處理完畢則執行緒恢復

  • WAITING :等待狀態
  • TIME_WAITING:超時等待狀態,超時以後自動返回
  • TERMINATED:終止狀態,表示當前執行緒執行完畢

如圖:

在這裡插入圖片描述

2.通過命令顯示執行緒狀態

  • jps: 開啟終端或者命令提示符,鍵入“jps”,(JDK1.5 提供的一個顯示當前所有 java
    程序 pid 的命令),可以獲得相應程序的 pid
  • jstack pid : 根據上一步驟獲得的 pid,繼續輸入 jstack pid(jstack 是 java 虛擬機器自帶的一種堆疊跟蹤工具。jstack 用於打印出給定的 java 程序 ID 或 core file 或遠端
    除錯服務的 Java 堆疊資訊)

3.執行緒啟動和停止

3.1 啟動

通過start方法實現

3.2 停止

1)使用 interrupt() 方法
  當其他執行緒通過呼叫當前執行緒的 interrupt 方法,表示向當前執行緒打個招呼,告訴他可以中斷執行緒的執行了,至於什麼時候中斷,取決於當前執行緒自己。執行緒通過檢查資深是否被中斷來進行相應,可以通過 isInterrupted()來判斷是否被中斷。另外使用interrupt會使阻塞的執行緒丟擲異常,注意看原始碼註釋

執行緒狀態復位
a: Thread.interrupted()
  執行緒中還提供了靜態方法 Thread.interrupted()對設定中斷標識的執行緒復位。比如一個的執行緒呼叫 thread.interrupt 來設定中斷標識,而另外一個執行緒,又通過Thread.interrupted 把執行緒的標識又進行了復位
b:丟擲 InterruptedException 異常
  還有一種被動復位的場景,就是對丟擲 InterruptedException 異常的方法,在InterruptedException 丟擲之前,JVM 會先把執行緒的中斷標識位清除,然後才會丟擲 InterruptedException,這個時候如果呼叫 isInterrupted 方法,將會返回 false。

2)定義一個 volatile 修飾的boolean成員變數

public class VolatileDemo {
	
	private volatile static boolean stop=false;
	
	public static void main(String[] args) throws InterruptedException {
		Thread thread=new Thread(()->{
			int i=0;
			while(!stop){
				i++;
			}
		});
	
	thread.start();
	System.out.println("begin start thread");
	Thread.sleep(1000);
	stop=true;
	}
}

4.執行緒安全問題

  執行緒安全問題可以總結為: 可見性、原子性、有序性

4.1 計算機底層執行緒原理

  執行緒是 CPU 排程的最小單元,執行緒涉及的目的最終仍然是更充分的利用計算機處理的效能。如:
在這裡插入圖片描述
  但是絕大部分的運算任務不能只依靠處理器“計算”就能完成,處理器還需要與記憶體互動,比如讀取運算資料、儲存運算結果,這個 I/O 操作是很難消除的。而由於計算機的儲存裝置與處理器的運算速度差距非常大,所以現代計算機系統都會增加一層讀寫速度儘可能接近處理器運算速度的快取記憶體來作為記憶體和處理器之間的緩衝:將運算需要使用的資料複製到快取中,讓運算能快速進行,當運算結束後再從快取同步到記憶體之中。如圖:
在這裡插入圖片描述

  快取記憶體從下到上越接近 CPU 速度越快,同時容量也越小。現在大部分的處理器都有二級或者三級快取,從下到上依次為 L3 cache, L2 cache, L1 cache. 快取又可以分為指令快取和資料快取,指令快取用來快取程式的程式碼,資料快取用來快取程式的資料

  • L1 Cache,一級快取,本地 core 的快取,分成 32K 的資料快取 L1d 和 32k 指 令快取 L1i,訪問 L1 需要3cycles,耗時大約 1ns;
  • L2 Cache,二級快取,本地 core 的快取,被設計為 L1 快取與共享的 L3 快取之間的緩衝,大小為 256K,訪問 L2 需要 12cycles,耗時大約 3ns;
  • L3 Cache,三級快取,在同插槽的所有 core共享 L3 快取,分為多個 2M 的 段,訪問 L3 需要 38cycles,耗時大約 12ns;
4.2 計算機底層快取一致性問題

  CPU-0 讀取主存的資料,快取到 CPU-0 的快取記憶體中,CPU-1 也做了同樣的事情,而 CPU-1 把 count 的值修改成了 2,並且同步到 CPU-1 的快取記憶體,但是這個修改以後的值並沒有寫入到主存中,CPU-0 訪問該位元組,由於快取沒有更新,所以仍然是之前的值,就會導致資料不一致的問題

  引發這個問題的原因是因為多核心 CPU 情況下存在指令並行執行,而各個CPU 核心之間的資料不共享從而導致快取一致性問題,為了解決這個問題,CPU 生產廠商提供了相應的解決方案看圖:
在這裡插入圖片描述

1)匯流排鎖
  當一個 CPU 對其快取中的資料進行操作的時候,往匯流排中傳送一個 Lock 訊號。其他處理器的請求將會被阻塞,那麼該處理器可以獨佔共享記憶體。匯流排鎖相當於把 CPU 和記憶體之間的通訊鎖住了,所以這種方式會導致 CPU 的效能下降,所以 P6 系列以後的處理器,出現了另外一種方式,就是快取鎖。

2)快取鎖
  如果快取在處理器快取行中的記憶體區域在 LOCK 操作期間被鎖定,當它執行鎖操作回寫記憶體時,處理不在總線上宣告 LOCK 訊號,而是修改內部的快取地址,然後通過快取一致性機制來保證操作的原子性,因為快取一致性機制會阻止同時修改被兩個以上處理器快取的記憶體區域的資料,當其他處理器回寫已經被鎖定的快取行的資料時會導致該快取行無效。

所以如果聲明瞭 CPU 的鎖機制,會生成一個 LOCK 指令,會產生兩個作用:

  1. Lock 字首指令會引起引起處理器快取回寫到記憶體,在 P6 以後的處理器中,LOCK 訊號一般不鎖匯流排,而是鎖快取
  2. 一個處理器的快取回寫到記憶體會導致其他處理器的快取無效

快取一致性協議
  處理器上有一套完整的協議,來保證 Cache 的一致性,比較經典的應該就是MESI 協議了,它的方法是在 CPU 快取中儲存一個標記位,這個標記為有四種狀態:

  • Ø M(Modified) 修改快取,當前 CPU 快取已經被修改,表示已經和記憶體中的資料不一致了
  • Ø I(Invalid) 失效快取,說明 CPU 的快取已經不能使用了
  • Ø E(Exclusive) 獨佔快取,當前 cpu 的快取和記憶體中資料保持一致,而且其他處理器沒有快取該資料
  • Ø S(Shared) 共享快取,資料和記憶體中資料一致,並且該資料存在多個 cpu快取中

每個 Core 的 Cache 控制器不僅知道自己的讀寫操作,也監聽其它 Cache 的讀
寫操作,嗅探(snooping)"協議

CPU 的讀取會遵循幾個原則:

  1. 如果快取的狀態是 I,那麼就從記憶體中讀取,否則直接從快取讀取
  2. 如果快取處於 M 或者 E 的 CPU 嗅探到其他 CPU 有讀的操作,就把自己的緩
    存寫入到記憶體,並把自己的狀態設定為 S
  3. 只有快取狀態是 M 或 E 的時候,CPU 才可以修改快取中的資料,修改後,緩
    存狀態變為 MC

CPU的優化執行
  除了增加快取記憶體以為,為了更充分利用處理器內內部的運算單元,處理器可能會對輸入的程式碼進行亂序執行優化,處理器會在計算之後將亂序執行的結果充足,保證該結果與順序執行的結果一直,但並不保證程式中各個語句計算的先後順序與輸入程式碼中的順序一致,這個是處理器的優化執行;還有一個就是程式語言的編譯器也會有類似的優化,比如做指令重排來提升效能。

4.3 Java中的執行緒安全問題

  比如快取一致性就導致可見性問題、處理器的亂序執行會導致原子性問題、指令重排會導致有序性問題。為了解決這些問題,所以在 JVM 中引入了 JMM 的概念。

4.3.1記憶體模型JMM

  記憶體模型定義了共享記憶體系統中多執行緒程式讀寫操作行為的規範,來遮蔽各種硬體和作業系統的記憶體訪問差異,來實現 Java 程式在各個平臺下都能達到一致的記憶體訪問效果。

  Java 記憶體模型的主要目標是定義程式中各個變數的訪問規則,也就是在虛擬機器中將變數儲存到記憶體以及從記憶體中取出變數(這裡的變數,指的是共享變數,也就是例項物件、靜態欄位、陣列物件等儲存在堆記憶體中的變數。而對於區域性變數這類的,屬於執行緒私有,不會被共享)這類的底層細節。通過這些規則來規範對記憶體的讀寫操作,從而保證指令執行的正確性。它與處理器有關、與快取有關、與併發有關、與編譯器也有關。他解決了 CPU多級快取、處理器優化、指令重排等導致的記憶體訪問問題,保證了併發場景下的可見性、原子性和有序性。

記憶體模型解決併發問題主要採用兩種方式:限制處理器優化和使用記憶體屏障

  Java 記憶體模型定義了執行緒和記憶體的互動方式,在 JMM 抽象模型中,分為主記憶體、工作記憶體。主記憶體是所有執行緒共享的,工作記憶體是每個執行緒獨有的。執行緒對變數的所有操作(讀取、賦值)都必須在工作記憶體中進行,不能直接讀寫主記憶體中的變數。並且不同的執行緒之間無法訪問對方工作記憶體中的變數,執行緒間的變數值的傳遞都需要通過主記憶體來完成,他們三者的互動關係如下
在這裡插入圖片描述

總結:
  所以,總的來說,JMM 是一種規範,目的是解決由於多執行緒通過共享記憶體進行通訊時,存在的本地記憶體資料不一致、編譯器會對程式碼指令重排序、處理器會對程式碼亂序執行等帶來的問題。目的是保證併發程式設計場景中的原子性、可見性和有序性。