1. 程式人生 > 程式設計 >【併發基礎】Java記憶體模型基礎知識

【併發基礎】Java記憶體模型基礎知識

1. 基本概念

1.1 程式

程式是一組計算機能識別和執行的指令,執行於電子計算機上,滿足人們某種需求的資訊化工具

1.2 程式

在早期面向程式設計的計算機結構中,程式是程式的基本執行實體;在當代面向執行緒設計的計算機結構中,程式是執行緒的容器

1.3 執行緒

是作業系統能夠進行運算排程的最小單位。大部分情況下,它被包含在程式之中,是程式中的實際運作單位。一條執行緒指的是程式中一個單一順序的控制流,一個程式中可以併發多個執行緒,每條執行緒並行執行不同的任務。

2. JVM與執行緒

2.1 JVM

Java虛擬機器器(英語:Java Virtual Machine,縮寫為JVM),一種能夠執行Java bytecode的虛擬機器器,以堆疊結構機器來進行實做。Java虛擬機器器有自己完善的硬體架構

,如處理器、堆疊、暫存器等,還具有相應的指令系統。JVM遮蔽了與具體作業系統平臺相關的資訊,使得Java程式只需生成在Java虛擬機器器上執行的目的碼(位元組碼),就可以在多種平臺上不加修改地執行。JVM作為一種程式語言的虛擬機器器,實際上不只是專用於Java語言,只要生成的編譯檔案符合JVM對載入編譯檔案格式要求,任何語言都可以由JVM編譯執行。

JVM執行

JVM在程式編譯好,正式啟動後開始執行,在程式結束時停止執行。這裡一個JVM是一個程式,裡麵包含了一個或多個執行緒。

3. JVM記憶體區域

JVM記憶體區域

3.1 堆區

JVM裡的“堆”(Heap)特指用於存放Java物件的記憶體區域。所以根據這個定義,Java物件全部都在堆上,且堆中不存放基本型別和物件引用,只存放物件本身。一個JVM只有一個堆區,它被所有執行緒共享,類的物件都由某種自動記憶體管理機制所管理,這種機制通常叫做“垃圾回收”或者GC(Garbage Collection)。如果在堆中沒有記憶體完成例項分配,並且堆也無法再擴充套件時,將會丟擲OutOfMemoryError異常

3.2 方法區

方法區(Method Area)與Java堆一樣,是各個執行緒共享的記憶體區域,它用於儲存已被虛擬機器器載入的類資訊、常量、靜態變數、即時編譯器編譯後的程式碼等資料。根據Java虛擬機器器規範的規定,當方法區無法滿足記憶體分配需求時,將丟擲OutOfMemoryError異常

3.3 VM Stack、棧區

JVM裡的“棧”(Stack)特指用於儲存方法中的基礎資料型別和自定義物件的引用(不是物件)的記憶體區域。每個執行緒包含一個棧區,棧區線上程建立時建立,它的生命期是跟隨執行緒的生命期,執行緒結束時棧區釋放。每個棧中的資料(原始型別和物件引用)都是私有的,其他棧不能訪問。棧分為3個部分:基本型別變數區、執行環境上下文、操作指令區(存放操作指令)。在Java虛擬機器器規範中,對這個區域規定了兩種異常狀況

:如果執行緒請求的棧深度大於虛擬機器器所允許的深度,將丟擲StackOverflowError異常;如果虛擬機器器棧可以動態擴充套件(當前大部分的Java虛擬機器器都可動態擴充套件,只不過Java虛擬機器器規範中也允許固定長度的虛擬機器器棧),當擴充套件時無法申請到足夠的記憶體時會丟擲OutOfMemoryError異常

3.4 Native Method Stacks、本地方法棧

本地方法棧與虛擬機器器棧所發揮的作用是非常相似的,其區別不過是虛擬機器器棧為虛擬機器器執行Java方法(也就是位元組碼)服務,而本地方法棧則是為虛擬機器器使用到的Native方法服務。

PC

這裡的PC不是指普通的電腦,而是程式計數器(Program Counter Register)的簡稱,它是一塊較小的記憶體空間,它的作用可以看做是當前執行緒所執行的位元組碼的行號指示器。在虛擬機器器的概念模型裡(僅是概念模型,各種虛擬機器器可能會通過一些更高效的方式去實現),位元組碼直譯器工作時就是通過改變這個計數器的值來選取下一條需要執行的位元組碼指令,分支、迴圈、跳轉、異常處理、執行緒恢復等基礎功能都需要依賴這個計數器來完成。

4. Java記憶體模型(JMM)

4.1 JMM

Java記憶體模型(英語:Java Memory Model,縮寫為JVM),一種虛擬機器器規範,用於遮蔽掉各種硬體和作業系統的記憶體訪問差異,以實現讓Java程式在各種平臺下都能達到一致的併發效果。JMM規範了Java虛擬機器器與計算機記憶體是如何協同工作的:規定了一個執行緒何時、如何可以看到由其他執行緒修改過後的共享變數的值,以及在必須時如何同步的訪問共享變數。

JMM

4.2 主記憶體

又叫共享記憶體,Java記憶體模型規定所有的變數都存在主記憶體中。

4.3 工作記憶體

又叫私有記憶體,每條執行緒都有自己的工作記憶體,執行緒對變數的操作都要在工作記憶體中進行,不同執行緒之間無法直接訪問對方工作記憶體中的變數。

4.4 主記憶體與工作記憶體的關係

Java記憶體模型的主要目標是定義程式中各個變數的訪問規則,即在虛擬機器器中將變數儲存到記憶體和從記憶體中取出變數這樣的底層細節。

此處的變數與Java程式設計中所說的變數有所區別,它包括了例項欄位、靜態欄位和構成陣列物件的元素,但不包括區域性變數與方法引數,因為後者是執行緒私有的,因此,不會被共享,自然就不存在競爭問題。

每條執行緒還有自己的工作記憶體,執行緒的工作記憶體中儲存了被該執行緒使用到的變數的主記憶體副本拷貝,執行緒對變數的所有操作(讀取、賦值等)都必須在工作記憶體中進行,而不能直接讀寫主記憶體中的變數。不同的執行緒之間也無法直接訪問對方工作記憶體中的變數,執行緒間變數值得傳遞均需要通過主記憶體來完成。

4.5 工作機制

  1. 修改私有資料

執行緒直接修改自己工作記憶體中的變數

  1. 修改共享資料

先把資料從主記憶體中拷貝到工作記憶體中,然後在工作記憶體中修改,修改完後,把資料再重新整理到主記憶體中

4.6 記憶體間互動操作

關於主記憶體與工作記憶體之間具體的交換協議,即一個變數如何從主記憶體拷貝到工作記憶體、如何從工作記憶體同步回主記憶體之類的實現細節,Java記憶體模型中定義了一下八種操作來完成,虛擬機器器實現時必須保證下面提及的每一種操作都是原子的、不可再分的。

操作 定義 概念
lock 鎖定 作用於主記憶體的變數,它把一個變數標識為一條執行緒獨佔的狀態
unlock 解鎖 作用與主記憶體的變數,它把一個處於鎖定狀態的變數釋放出來,釋放後的變數才可以被其他執行緒鎖定
read 讀取 作用於主記憶體的變數,它把一個變數的值從主記憶體傳輸到執行緒的工作記憶體中,以便隨後的load動作使用
load 載入 作用於工作記憶體的變數,它把read操作從主記憶體中得到的變數值放入工作記憶體的變數副本中
use 使用 作用於工作記憶體的變數,它把工作記憶體中一個變數的值傳遞給執行引擎,每當虛擬機器器遇到一個需要使用到變數的值的位元組碼指令時將會執行這個操作
assign 賦值 作用於工作記憶體的變數,它把一個從執行引擎接收到的值賦給工作記憶體的變數,每當虛擬機器器遇到一個給變數賦值的位元組碼指令時將會執行這個操作
store 儲存 作用於工作記憶體的變數,它把工作記憶體中一個變數的值傳送到主記憶體中,以便隨後的write操作使用
write 寫入 作用於主記憶體的變數,它把store操作從工作記憶體中得到的變數的值放入主記憶體的變數中

其中,read load、store write必須按順序執行,但不保證是連續執行。這些定義相當嚴謹但又十分繁瑣,實踐起來很麻煩,所以我們一個等效判斷原則-------先行發生原則,用來確定一個訪問在併發環境下是否安全。

5. 併發程式設計

5.1 併發

是指一個時間段中有幾個程式都處於已啟動執行到執行完畢之間,且這幾個程式都是在同一個處理機上執行,但任一個時刻點上只有一個程式在處理機上執行。

5.2 執行緒安全

在擁有共享資料的多條執行緒並行執行的程式中,執行緒安全的程式碼會通過同步機制保證各個執行緒都可以正常且正確的執行,不會出現資料汙染等意外情況。

5.3 場景

現在有一個搶票系統,大家過節都想回家,但票只有那麼多,所以早上十點開始售票的時候就有很多人去買票,假設總共有4張票,B要買2張票,C要買4張票,剛好他提交的資料同時到了伺服器,由於伺服器同時收到兩個請求,所以現在建立了兩個執行緒BB,CC來處理B和C的請求,兩個執行緒同時從主記憶體中獲取資料,那麼兩個執行緒都以為還剩4張票,BB認為滿足B的需求,CC認為滿足C的需求,所以兩條執行緒都把票賣出去了,結果賣出去6張,那麼B和C的票肯定是重複的,這就不符合實際的業務需求。

其中,B、C同時搶票這個概念稱為併發情況,而在剩餘的4張票裡面搶票出現了搶出了6張票這種情況我們認為是執行緒不安全的。

5.4 併發編與JMM

Java記憶體模型是圍繞著在併發過程中如何處理原子性、可見性和有序性這3個特徵來建立的。

5.4.1 三大特性

  1. 原子性
    1. 由Java記憶體模型來直接保證的原子性變數操作包括read、load、assign、use、store和write,我們大致可以認為基本資料型別的訪問讀寫是具備原子性的。
  2. 可見性
    1. 可見性是指當一個執行緒修改了共享變數的值,其他執行緒能夠立即得知這個修改。
  3. 有序性
    1. 因為“執行緒內表現為序列的語義”,所以如果在本執行緒內觀察,所有的操作都是有序的;
    2. 因為“指令重排序”現象和“工作記憶體與主記憶體同步延遲”現象,所以如果在一個執行緒中觀察另一個執行緒,所有的操作都是無序的。

5.4.2 先行發生原則

先行發生原則是判斷資料是否存在競爭、執行緒是否安全的主要依據,它是指 Java 記憶體模型中定義的兩項操作之間的偏序關係,如果說操作A先行發生於操作B,其實就是說在發生操作B之前,操作A產生的影響能被操作B觀察到。

下面是Java記憶體模型下一些“天然的”先行發生關係,這些先行發生關係無需任何同步器協助就已經存在。

  1. 程式次序規則:在一個執行緒內,按照程式程式碼順序,書寫在前面的操作先行發生於書寫在後面的操作。
  2. 管理鎖定規則:一個 unlock 操作先行發生於後面對同一個鎖的lock操作。這裡必須強調是同一個鎖,而“後面”是指時間上的先後順序。
  3. volatile變數規則:對一個 volatile 變數的寫操作先行發生於後面對這個變數的讀操作。“後面”是指時間上的先後順序。
  4. 執行緒啟動規則:Thread 物件的 start() 方法先行發生於此執行緒的每一個動作。
  5. 執行緒終止規則:執行緒中的所有操作都先行發生於對此執行緒的終止檢測。
  6. 執行緒中斷規則:對執行緒 interrupt() 方法的調優先行發生於被中斷執行緒的程式碼檢測到中斷事件的發生。
  7. 物件終結規則:一個物件的初始化完成(建構函式執行結束)先行發生於它的 finalize() 方法的開始。
  8. 傳遞性:如果操作A先行發生於操作B,操作B先行發生於操作C,那就可以得出結論操作A先生髮生於操作C的結論。

6. Java執行緒排程

執行緒排程是指系統為執行緒分配處理器使用權的過程,主要排程方式有兩種,分別是協同式排程和搶佔式排程。

6.1 協同式排程

如果使用協同式排程的多執行緒系統,執行緒的執行時間由執行緒本身控制,執行緒把自己的工作執行完了之後,就主動通知系統切換到另一個執行緒上。

協同式排程的最大好處是實現簡單,而且由於執行緒要把自己的事情幹完後才會進行執行緒切換,切換操作對執行緒自己是可知的,所以沒有什麼執行緒同步的問題。

同時,它的壞處是:執行緒執行時間不可控制,甚至如果一個執行緒編寫有問題,一直不告訴系統進行執行緒切換,那麼程式就會一直阻塞在那裡。

6.2 搶佔式排程

如果使用搶佔式排程的多執行緒系統,那麼每個執行緒將由系統來分配執行時間,執行緒的切換不由執行緒本身來決定(在Java中,Thread.yield() 可以讓出執行時間)。在這種實現執行緒排程的方式下,執行緒的執行時間是系統可控的,也不會有一個執行緒導致整個程式阻塞的問題,Java使用的執行緒排程方式就是搶佔式排程。例如 Windows ,當一個程式出現問題,我們還可以使用工作管理員把 這個程式“殺掉”。

雖然Java執行緒的排程室系統自動完成的,但是我們還可以“建議”系統給某些執行緒多分配一點執行時間,另外的一下執行緒則可以少分配一點-----這項操作可以通過設定執行緒優先順序來完成。

在兩個執行緒同時處於Ready狀態時,優先順序越高的執行緒越容易被系統選擇執行。

不過執行緒遊戲家也並不是太靠譜,原因是Java的執行緒是通過對映到系統的原生執行緒上來實現的,所以執行緒排程最終還是取決於作業系統,在Windows系統存在一個成為“優先順序推進器”的功能,它的大致作用就是當系統發現一個執行緒執行得特別“勤奮努力”的話,可能會越過執行緒優先順序去為它分配執行時間。

7. 執行緒狀態

Java語言定義了5種執行緒狀態,在任意個時間點,一個執行緒只能有且只有其中的一種狀態。

7.1 新建(New)

建立後尚未啟動的執行緒處於這種狀態。

7.2 執行(Runable)

包括了作業系統執行緒狀態中的 Running 和 Ready,也就是處於次狀態的執行緒有可能正在執行,也有可能正在等待著 CPU 為它分配執行時間。

7.3 無限期等待 (Waiting)

執行緒不會被分配CPU執行時間,它們要等待被其他執行緒顯示地喚醒,以下方法會讓執行緒陷入無限期的等待狀態。

  1. 沒有設定 Timeout 引數的 Object.wait() 方法。
  2. 沒有設定 Timeout 引數的 Thread.join() 方法。
  3. LockSupport.park() 方法

7.4 有限期等待(Timed Waiting)

執行緒也不會被分配 CPU 執行時間,不過無需等待被其他執行緒顯示的喚醒,在一定時間之後它們會由系統自動喚醒。以下方法會讓執行緒陷入有限期的等待狀態。

  1. Thread.sleep() 方法
  2. 設定 Timeout 引數的 Object.wait() 方法。
  3. 設定 Timeout 引數的 Thread.join() 方法。
  4. LockSupport.parkNanos()
  5. LockSupport.parkUnitl()

7.5 阻塞(Blocked)

執行緒被阻塞了,“阻塞狀態”與“等待狀態”的區別是:阻塞狀態在等待著獲取一個排它鎖,這個時間將在另外一個執行緒放棄這個鎖的時候發生;而“等待狀態”則是在等待一段時間,或者喚醒動作的發生,在程式等待進入同步區域的時候,執行緒將進入這種狀態。

7.6 結束

已終止的執行緒狀態。