Java 並發的基本概念#
硬體層次#
由於 CPU 執行指令的速度是很快的,但是內存訪問的速度就慢了很多,相差的不是一個數量級,於是為了不讓內存成為計算機程序處理的瓶頸,透過在 CPU 和內存之間增加高速緩存的方式來解決
緩存一致性#
在 CPU 和主存之間增加緩存,在多線程場景下就可能存在緩存一致性問題,也就是說,在多核 CPU 中,每個核的自己的緩存中,關於同一個數據的緩存內容可能不一致
解決緩存一致性的兩種方案
- 透過在總線加 LOCK# 鎖的方式 (現代計算機都是多核 CPU,總線加鎖會導致其他 CPU 也無法訪問內存,效率低下)
- 透過緩存一致性協議(Cache Coherence Protocol)
MESI 緩存一致性協議#
緩存一致性協議(Cache Coherence Protocol),最出名的就是 Intel 的 MESI 協議,MESI 協議保證了每個緩存中使用的共享變量的副本是一致的
MESI 的核心的思想是:當 CPU 寫數據時,如果發現操作的變量是共享變量,即在其他 CPU 中也存在該變量的副本,會發出信號通知其他 CPU 將該變量的緩存行置為無效狀態,因此當其他 CPU 需要讀取這個變量時,發現自己緩存中緩存該變量的緩存行是無效的,那麼它就會從內存重新讀取
在 MESI 協議中,每個緩存可能有 4 個狀態,它們分別是:
M(Modified)
:這行數據有效,數據被修改了,和內存中的數據不一致,數據只存在於本 Cache 中。E(Exclusive)
:這行數據有效,數據和內存中的數據一致,數據只存在於本 Cache 中。S(Shared)
:這行數據有效,數據和內存中的數據一致,數據存在於很多 Cache 中。I(Invalid)
:這行數據無效
MESI 協議,可以保證緩存的一致性,但是無法保證實時性
處理器優化和指令重排#
處理器優化
: 為了使處理器內部的運算單元能夠盡量的被充分利用,處理器可能會對輸入代碼進行亂序執行處理指令重排
: 除了現在很多流行的處理器會對代碼進行優化亂序處理,很多編程語言的編譯器也會有類似的優化,比如 Java 虛擬機的即時編譯器(JIT)也會做指令重排- 聯想記憶到:
- Spark 中不存在依賴關係的 task 並發執行優化計算
- 不存在資源競爭的程序並發執行,比如某個程序搶佔 IO 資源,那麼可以先去執行其他不搶佔 IO 資源的程序,省卻等待時間
並發編程中的三個概念#
原子性#
原子性是指在一個操作中就是 cpu 不可以在中途暫停然後再調度,既不被中斷操作,要不執行完成,要不就不執行 (聯想記憶到數據庫事務處理的原子性)
可見性#
可見性是指當多個線程訪問同一個變量時,一個線程修改了這個變量的值,其他線程能夠立即看得到修改的值
有序性#
有序性即程序執行的順序按照代碼的先後順序執行
其實,原子性問題,可見性問題和有序性問題。是人們抽象定義出來的。而這個抽象的底層問題就是前面提到的緩存一致性問題、處理器優化問題和指令重排問題等。
緩存一致性問題其實就是可見性問題,而處理器優化
可能會造成導致原子性問題的,指令重排
即會導致有序性問題
Java 的內存模型#
基本概念#
-
Java 的並發採用 “共享內存” 模型,線程之間透過讀寫內存的公共狀態進行通訊。多個線程之間是不能透過直接傳遞數據交互的,它們之間交互只能透過共享變量實現。
-
JMM 本身是一種抽象的概念,並不真實存在,它描述的是一組規則或規範,規定所有變量都是存在主存中的,類似於
普通內存
,每個線程又包含自己的工作內存,類比高速緩存
。所以線程的操作都是以工作內存為主,它們只能訪問自己的工作內存,且工作前後都要把值在同步回主內存
ps: 聯想到有點類似於緩存 + DB 的系統架構,所有變量都是存在主存中的,類似於DB
, 每個線程又包含自己的工作內存,類比高速緩存
Java 內存模型的實現#
-
Java 內存模型 (JMM) 規定所有變量都存儲在主內存中,每個線程還有自己的工作內存:
- 線程的工作內存中保存了被該線程使用到的變量的拷貝(從主內存中拷貝過來),線程對變量的所有操作都必須在工作內存中執行,而不能直接訪問主內存中的變量。
- 不同線程之間無法直接訪問對方工作內存的變量,線程間變量值的傳遞都要透過主內存來完成。
-
Java 線程之間的通信由內存模型 JMM(Java Memory Model)控制:
- JMM 決定一個線程對變量的寫入何時對另一個線程可見。
- 線程之間共享變量存儲在主內存中
- 每個線程有一個私有的本地內存,裡面存儲了讀 / 寫共享變量的副本。
- JMM 透過控制每個線程的本地內存之間的交互,來為程序員提供內存可見性保證。
-
內存間交互操作:
- lock(鎖定):作用於主內存的變量,把一個變量標識為一條線程獨占狀態。
- unlock(解鎖):作用於主內存的變量,把一個處於鎖定狀態的變量釋放出來,釋放後的變量才可以被其他線程鎖定。
- read(讀取):作用於主內存變量,把主內存的一個變量讀取到工作內存中。
- load(載入):作用於工作內存,把 read 操作讀取到工作內存的變量載入到工作內存的變量副本中
- use(使用):作用於工作內存的變量,把工作內存中的變量值傳遞給一個執行引擎。
- assign(賦值):作用於工作內存的變量。把執行引擎接收到的值賦值給工作內存的變量。
- store(存儲):把工作內存的變量的值傳遞給主內存
- write(寫入):把 store 操作的值入到主內存的變量中
注意:主內存和工作內存與 JVM 內存結構中的 Java 堆、棧、方法區等並不是同一個層次的內存劃分
Java 中並發的實現#
原子性的實現#
-
在 Java 中,為了保證原子性,提供了兩個高級的字節碼指令 monitorenter 和 monitorexit,對應的關鍵字就是 synchronized
-
Atomic 類也可以實現原子性
基於 CAS 原理,參考為什麼 volatile 不能保證原子性而 Atomic 可以
可見性的實現#
參考: 並發三特性 - 可見性定義、可見性問題與可見性保證技術
-
透過 volatile 關鍵字標記內存屏障保證可見性。
-
透過 synchronized 關鍵字定義同步代碼塊或者同步方法保障可見性。
-
透過 Lock 接口保障可見性。
-
透過 Atomic 類型保障可見性
-
透過 final 關鍵字實現
被 final 修飾的字段一旦初始化完成 (靜態變量或者在構造函數中初始化),並且構造器沒有把 “this” 的引用傳遞出去(this 引用逃逸是一件很危險的事情,其它線程有可能透過這個引用訪問到 “初始化了一半” 的對象),那在其他線程中就能看見 final 字段的值
有序性的實現#
在 Java 中,可以使用 synchronized 和 volatile 來保證多線程之間操作的有序性
- volatile 關鍵字會禁止指令重排
- synchronized 關鍵字保證同一時刻只允許一條線程操作
happens-before原則
#
JMM 具備一些先天的有序性,即不需要透過任何手段就可以保證的有序性,通常稱為 happens-before 原則. 《JSR-133:Java Memory Model and Thread Specification》定義了如下 happens-before 規則:
程序順序規則
: 即在一個線程內必須保證語義串行性,也就是說按照代碼順序執行監視器鎖規則
:對一個線程的解鎖,happens-before 於隨後對這個線程的加鎖volatile變量規則
: 對一個 volatile 域的寫,happens-before 於後續對這個 volatile 域的讀傳遞性
:如果 A happens-before B , 且 B happens-before C, 那麼 A happens-before Cstart()規則
: 線程的 start () 方法先於它的每一個動作,即如果線程 A 在執行線程 B 的 start 方法之前修改了共享變量的值,那麼當線程 B 執行 start 方法時,線程 A 對共享變量的修改對線程 B 可見join()線程終止原則
: 線程的所有操作先於線程的終結,如果 A 執行 ThreadB.join () 並且成功返回,那麼線程 B 中的任意操作 happens-before 於線程 A 從 ThreadB.join () 操作成功返回。interrupt()線程中斷原則
: 對線程 interrupt () 方法的調用先行發生於被中斷線程代碼檢測到中斷事件的發生,可以透過 Thread.interrupted () 方法檢測是否有中斷發生finalize()對象終結原則
:一個對象的初始化完成先行發生於它的 finalize () 方法的開始