天天看點

小白也能看懂的Java記憶體模型前言内容大綱硬體記憶體模型Java記憶體模型關于我

前言

Java并發程式設計系列開坑了,Java并發程式設計可以說是中進階研發工程師的必備素養,也是中進階崗位面試必問的問題,本系列就是為了帶讀者們系統的一步一步擊破Java并發程式設計各個難點,打破屏障,在面試中所向披靡,拿到心儀的offer,Java并發程式設計系列文章依然采用圖文并茂的風格,讓小白也能秒懂。

Java記憶體模型(

Java Memory Model

)簡稱

J M M

,作為Java并發程式設計系列的開篇,它是Java并發程式設計的基礎知識,了解它能讓你更好的明白線程安全到底是怎麼一回事。

内容大綱

小白也能看懂的Java記憶體模型前言内容大綱硬體記憶體模型Java記憶體模型關于我

硬體記憶體模型

程式是指令與資料的集合,計算機執行程式時,是

C P U

在執行每條指令,因為

C P U

要從記憶體讀指令,又要根據指令訓示去記憶體讀寫資料做運算,是以執行指令就免不了與記憶體打交道,早期記憶體讀寫速度與

C P U

處理速度差距不大,倒沒什麼問題。

C P U緩存

随着

C P U

技術快速發展,

C P U

的速度越來越快,記憶體卻沒有太大的變化,導緻記憶體的讀寫(

IO

)速度與

C P U

的處理速度差距越來越大,為了解決這個問題,引入了緩存(

Cache

)的設計,在

C P U

與記憶體之間加上緩存層,這裡的緩存層就是指

C P U

内的寄存器與高速緩存(

L1,L2,L3

小白也能看懂的Java記憶體模型前言内容大綱硬體記憶體模型Java記憶體模型關于我

從上圖中可以看出,寄存器最快,主内最慢,越快的存儲空間越小,離

C P U

越近,相反存儲空間越大速度越慢,離

C P U

越遠。

C P U如何與記憶體互動

C P U

運作時,會将指令與資料從主存複制到緩存層,後續的讀寫與運算都是基于緩存層的指令與資料,運算結束後,再将結果從緩存層寫回主存。

小白也能看懂的Java記憶體模型前言内容大綱硬體記憶體模型Java記憶體模型關于我

上圖可以看出,

C P U

基本都是在和緩存層打交道,采用緩存設計彌補主存與

C P U

處理速度的差距,這種設計不僅僅展現在硬體層面,在日常開發中,那些并發量高的業務場景都能看到,但是凡事都有利弊,緩存雖然加快了速度,同樣也帶來了在多線程場景存在的緩存一緻性問題,關于緩存一緻性問題後面會說,這裡大家留個印象。

Java記憶體模型

Java Memory Model,J M M

),後續都以

J M M

簡稱,

J M M

是建立在硬體記憶體模型基礎上的抽象模型,并不是實體上的記憶體劃分,簡單說,為了使

Java

虛拟機(

Java Virtual Machine,J V M

)在各平台下達到一緻的記憶體互動效果,需要屏蔽下遊不同硬體模型的互動差異,統一規範,為上遊提供統一的使用接口。

J M M

是保證

J V M

在各平台下對計算機記憶體的互動都能保證效果一緻的機制及規範。

小白也能看懂的Java記憶體模型前言内容大綱硬體記憶體模型Java記憶體模型關于我

抽象結構

J M M

抽象結構劃分為線程本地緩存與主存,每個線程均有自己的本地緩存,本地緩存是線程私有的,主存則是計算機記憶體,它是共享的。

小白也能看懂的Java記憶體模型前言内容大綱硬體記憶體模型Java記憶體模型關于我

不難發現

J M M

與硬體記憶體模型差别不大,可以簡單的把線程類比成Core核心,線程本地緩存類比成緩存層,如下圖所示

小白也能看懂的Java記憶體模型前言内容大綱硬體記憶體模型Java記憶體模型關于我

雖然記憶體互動規範好了,但是多線程場景必然存線上程安全問題(競争共享資源),為了使多線程能正确的同步執行,就需要保證并發的三大特性可見性、原子性、有序性。

可見性

當一個線程修改了共享變量的值,其他線程能夠立即得知這個修改,這就是可見性,如果無法保證,就會出現緩存一緻性的問題,

J M M

規定,所有的變量都放在主存中,當線程使用變量時,先從緩存中擷取,緩存未命中,再從主存複制到緩存,最終導緻線程操作的都是自己緩存中的變量。

小白也能看懂的Java記憶體模型前言内容大綱硬體記憶體模型Java記憶體模型關于我

線程A執行流程

  • 線程

    A

    從緩存擷取變量

    a

  • 緩存未命中,從主存複制到緩存,此時

    a

  • A

    擷取變量

    a

    ,執行計算
  • 計算結果

    1

    ,寫入緩存
  • 1

    ,寫入主存

線程B執行流程

  • B

    a

  • a

    1

  • B

    擷取變量a,執行計算
  • 2

  • 2

A

B

兩個線程執行完後,線程

A

與線程

B

緩存資料不一緻,這就是緩存一緻性問題,一個是

1

,另一個是

2

,如果線程

A

再進行一次

+1

操作,寫入主存的還是

2

,也就是說兩個線程對

a

共進行了

3

+1

,期望的結果是

3

,最終得到的結果卻是

2

解決緩存一緻性問題,就要保證可見性,思路也很簡單,變量寫入主存後,把其他線程緩存的該變量清空,這樣其他線程緩存未命中,就會去主存加載。

小白也能看懂的Java記憶體模型前言内容大綱硬體記憶體模型Java記憶體模型關于我
  • A

    a

  • a

  • A

    a

  • 1

  • 1

    ,寫入主存,并清空線程

    B

    緩存

    a

    變量
  • B

    a

  • a

    1

  • B

  • 2

  • 2

    A

    a

A

B

A

緩存是空的,此時線程A再進行一次

+1

操作,會從主存加載(先從緩存中擷取,緩存未命中,再從主存複制到緩存)得到

2

,最後寫入主存的是

3

Java

中提供了

volatile

修飾變量保證可見性(本文重點是

J M M

,是以不會對

volatile

做過多的解讀)。

看似問題都解決了,然而上面描述的場景是建立在理想情況(線程有序的執行),實際中線程可能是并發(交替執行),也可能是并行,隻保證可見性仍然會有問題,是以還需要保證原子性。

原子性

原子性是指一個或者多個操作在

C P U

執行的過程中不被中斷的特性,要麼執行,要不執行,不能執行到一半,為了直覺的了解什麼是原子性,看看下面這段代碼

int a=0;
a++;           
  • 原子性操作:

    int a=0

    隻有一步操作,就是指派
  • 非原子操作:

    a++

    有三步操作,讀取值、計算、指派

如果多線程場景進行

a++

操作,僅保證可見性,沒有保證原子性,同樣會出現問題。

小白也能看懂的Java記憶體模型前言内容大綱硬體記憶體模型Java記憶體模型關于我

并發場景(線程交替執行)

  • A

    讀取變量

    a

    到緩存,

    a

  • 進行

    +1

    運算得到結果

    1

  • 切換到

    B

  • B

    線程執行完整個流程,

    a=1

    寫入主存
  • A

    恢複執行,把結果

    a=1

    寫入緩存與主存
  • 最終結果錯誤

并行場(線程同時執行)

  • A

    B

    同時執行,可能線程

    A

    執行運算

    +1

    的時候,線程

    B

    就已經全部執行完成,也可能兩個線程同時計算完,同時寫入,不管是那種,結果都是錯誤的。

為了解決此問題,隻要把多個操作變成一步操作,即保證原子性。

小白也能看懂的Java記憶體模型前言内容大綱硬體記憶體模型Java記憶體模型關于我

Java

synchronized

(同時滿足有序性、原子性、可見性)可以保證結果的原子性(注意這裡的描述),

synchronized

保證原子性的原理很簡單,因為

synchronized

可以對代碼片段上鎖,防止多個線程并發執行同一段代碼(本文重點是

J M M

synchronized

小白也能看懂的Java記憶體模型前言内容大綱硬體記憶體模型Java記憶體模型關于我

并發場景(線程

A

B

交替執行)

  • A

    擷取鎖成功
  • A

    a

    到緩存,進行

    +1

    1

  • 此時切換到了

    B

  • B

    擷取鎖失敗,阻塞等待
  • 切換回線程

    A

  • A

    執行完所有流程,主存

    a=1

  • 線程A釋放鎖成功,通知線程

    B

    擷取鎖
  • 線程B擷取鎖成功,讀取變量

    a

    到緩存,此時

    a=1

  • 線程B執行完所有流程,主存

    a=2

  • 線程B釋放鎖成功

并行場景

  • A

  • B

  • A

    a

    +1

    1

  • A

    a=1

  • A

    釋放鎖成功,通知線程

    B

  • B

    擷取鎖成功,讀取變量

    a

    a=1

  • B

    a=2

  • B

    釋放鎖成功

synchronized

對共享資源代碼段上鎖,達到互斥效果,天然的解決了無法保證原子性、可見性、有序性帶來的問題。

雖然在并行場

A

線程還是被中斷了,切換到了

B

線程,但它依然需要等待

A

線程執行完畢,才能繼續,是以結果的原子性得到了保證。

有序性

在日常搬磚寫代碼時,可能大家都以為,程式運作時就是按照編寫順序執行的,但實際上不是這樣,編譯器和處理器為了優化性能,會對代碼做重排,是以語句實際執行的先後順序與輸入的代碼順序可能一緻,這就是指令重排序。

可能讀者們會有疑問“指令重排為什麼能優化性能?”,其實

C P U

會對重排後的指令做并行執行,達到優化性能的效果。

重排序前的指令

小白也能看懂的Java記憶體模型前言内容大綱硬體記憶體模型Java記憶體模型關于我

重排序後的指令

小白也能看懂的Java記憶體模型前言内容大綱硬體記憶體模型Java記憶體模型關于我

重排序後,對

a

操作的指令發生了改變,節省了一次

Load a

Store a

,達到性能優化效果,這就是重排序帶來的好處。

重排遵循

as-if-serial

原則,編譯器和處理器不會對存在資料依賴關系的操作做重排序,因為這種重排序會改變執行結果(即不管怎麼重排序,單線程程式的執行結果不能被改變),下面這種情況,就屬于資料依賴。

int i = 10
int j = 10
//這就是資料依賴,int i 與 int j 不能排到 int c下面去
int c = i + j           

但也僅僅隻是針對單線程,多線程場景可沒這種保證,假設

A、B

兩個線程,線程

A

代碼段無資料依賴,線程

B

依賴線程

A

的結果,如下圖(假設保證了可見性)

小白也能看懂的Java記憶體模型前言内容大綱硬體記憶體模型Java記憶體模型關于我

禁止重排場景(i預設0)

  • A

    執行

    i = 10

  • A

    b = true

  • B

    if( b )

    通過驗證
  • B

    i = i + 10

  • 最終結果

    i

    20

重排場景(i預設0)

  • A

    b = true

  • B

    if( b )

  • B

    i = i + 10

  • A

    i = 10

  • i

    10

為解決重排序,使用Java提供的

volatile

修飾變量同時保證可見性、有序性,被

volatile

修飾的變量會加上記憶體屏障禁止排序(本文重點是

J M M

volatile

三大特性的保證

特性 volatile synchronized Lock Atomic
可以保證
無法保證
一定程度保證

關于我

這裡是阿星,一個熱愛技術的Java程式猿,公衆号 「程式猿阿星」 裡将會定期分享作業系統、計算機網絡、Java、分布式、資料庫等精品原創文章,2021,與您在 Be Better 的路上共同成長!。

繼續閱讀