專欄原創(chuàng)出處:github-源筆記文件 间护,github-源碼 是己,歡迎 Star雳攘,轉(zhuǎn)載請附上原文出處鏈接和本聲明带兜。
基礎(chǔ)概念
- 原子性:即一個操作或者多個操作 要么全部執(zhí)行并且執(zhí)行的過程不會被任何因素打斷,要么就都不執(zhí)行
- 可見性:指當(dāng)多個線程訪問同一個變量時吨灭,一個線程修改了這個變量的值刚照,其他線程能夠立即看得到修改的值
- 有序性:即程序執(zhí)行的順序按照代碼的先后順序執(zhí)行
線程之間如何通信?
命令式編程中線程通信的方式:
- 共享內(nèi)存
- 消息傳遞
在共享內(nèi)存模型里線程之間共享內(nèi)存的公共狀態(tài)沃于,在消息傳遞模型里涩咖,線程之間靠消息的發(fā)送接收來顯示的進(jìn)行通信海诲。 Java 使用共享內(nèi)存模型進(jìn)行線程通信。
Java 內(nèi)存模型定義是檩互?
Java 內(nèi)存模型(Java Memory Model特幔,JMM)用于屏蔽掉各種硬件和操作系統(tǒng)的內(nèi)存訪問差異,以實現(xiàn)讓 Java 程序在各種平臺下都能達(dá)到一致的并發(fā)效果闸昨,JMM 規(guī)范了 Java 虛擬機與計算機內(nèi)存是如何協(xié)同工作的:規(guī)定了一個線程如何和何時可以看到由其他線程修改過后的共享變量的值蚯斯,以及在必須時如何同步的訪問共享變量。
現(xiàn)代硬件內(nèi)存架構(gòu)
現(xiàn)代硬件內(nèi)存模型與 Java 內(nèi)存模型有一些不同饵较,理解內(nèi)存模型架構(gòu)以及 Java 內(nèi)存模型如何與它協(xié)同工作也是非常重要的∨那叮現(xiàn)代計算機硬件架構(gòu)的簡單圖示:
- 多 CPU:一個現(xiàn)代計算機通常由兩個或者多個 CPU。其中一些 CPU 還有多核循诉。從這一點可以看出横辆,在一個有兩個或者多個 CPU 的現(xiàn)代計算機上同時運行多個線程是可能的。每個 CPU 在某一時刻運行一個線程是沒有問題的茄猫。這意味著狈蚤,如果你的 Java 程序是多線程的,在你的 Java 程序中每個 CPU 上一個線程可能同時(并發(fā))執(zhí)行划纽。
- CPU 寄存器:每個 CPU 都包含一系列的寄存器脆侮,它們是 CPU 內(nèi)內(nèi)存的基礎(chǔ)。CPU 在寄存器上執(zhí)行操作的速度遠(yuǎn)大于在主存上執(zhí)行的速度勇劣。這是因為 CPU 訪問寄存器的速度遠(yuǎn)大于主存靖避。
- 高速緩存 cache:由于計算機的存儲設(shè)備與處理器的運算速度之間有著幾個數(shù)量級的差距,所以現(xiàn)代計算機系統(tǒng)都不得不加入一層讀寫速度盡可能接近處理器運算速度的高速緩存(Cache)來作為內(nèi)存與處理器之間的緩沖:將運算需要使用到的數(shù)據(jù)復(fù)制到緩存中比默,讓運算能快速進(jìn)行幻捏,當(dāng)運算結(jié)束后再從緩存同步回內(nèi)存之中,這樣處理器就無須等待緩慢的內(nèi)存讀寫了退敦。CPU 訪問緩存層的速度快于訪問主存的速度粘咖,但通常比訪問內(nèi)部寄存器的速度還要慢一點。每個 CPU 可能有一個 CPU 緩存層侈百,一些 CPU 還有多層緩存瓮下。在某一時刻,一個或者多個緩存行(cache lines)可能被讀到緩存钝域,一個或者多個緩存行可能再被刷新回主存讽坏。
- 內(nèi)存:一個計算機還包含一個主存。所有的 CPU 都可以訪問主存例证。主存通常比 CPU 中的緩存大得多路呜。
- 運作原理:通常情況下,當(dāng)一個 CPU 需要讀取主存時,它會將主存的部分讀到 CPU 緩存中胀葱。它甚至可能將緩存中的部分內(nèi)容讀到它的內(nèi)部寄存器中漠秋,然后在寄存器中執(zhí)行操作。當(dāng) CPU 需要將結(jié)果寫回到主存中去時抵屿,它會將內(nèi)部寄存器的值刷新到緩存中庆锦,然后在某個時間點將值刷新回主存。
多線程環(huán)境下一些問題:
- 緩存一致性問題:在多處理器系統(tǒng)中轧葛,每個處理器都有自己的高速緩存搂抒,而它們又共享同一主內(nèi)存(MainMemory)∧虺叮基于高速緩存的存儲交互很好地解決了處理器與內(nèi)存的速度矛盾求晶,但是也引入了新的問題:緩存一致性(CacheCoherence)。當(dāng)多個處理器的運算任務(wù)都涉及同一塊主內(nèi)存區(qū)域時衷笋,將可能導(dǎo)致各自的緩存數(shù)據(jù)不一致的情況芳杏,如果真的發(fā)生這種情況,那同步回到主內(nèi)存時以誰的緩存數(shù)據(jù)為準(zhǔn)呢右莱?為了解決一致性的問題蚜锨,需要各個處理器訪問緩存時都遵循一些協(xié)議,在讀寫時要根據(jù)協(xié)議來進(jìn)行操作慢蜓,這類協(xié)議有 MSI、MESI(IllinoisProtocol)郭膛、MOSI晨抡、Synapse、Firefly 及 DragonProtocol则剃,等等
- 指令重排序問題:為了使得處理器內(nèi)部的運算單元能盡量被充分利用耘柱,處理器可能會對輸入代碼進(jìn)行亂序執(zhí)行(Out-Of-Order Execution)優(yōu)化,處理器會在計算之后將亂序執(zhí)行的結(jié)果重組棍现,保證該結(jié)果與順序執(zhí)行的結(jié)果是一致的调煎,但并不保證程序中各個語句計算的先后順序與輸入代碼中的順序一致。因此己肮,如果存在一個計算任務(wù)依賴另一個計算任務(wù)的中間結(jié)果士袄,那么其順序性并不能靠代碼的先后順序來保證。與處理器的亂序執(zhí)行優(yōu)化類似谎僻,Java 虛擬機的即時編譯器中也有類似的指令重排序(Instruction Reorder)優(yōu)化
Java 內(nèi)存模型結(jié)構(gòu) (JMM)
Java 堆和方法區(qū)是多個線程共享的數(shù)據(jù)區(qū)域娄柳。多個線程可以操作堆和方法區(qū)中的同一個數(shù)據(jù)。局部變量艘绍,方法定義參數(shù)和異常處理參數(shù)不會在線程之間共享赤拒,它們不會有內(nèi)存可見性問題,也不受內(nèi)存模型影響。
Java 內(nèi)存模型的英文名稱為 Java Memory Model(JMM)挎挖,其并不像 JVM 內(nèi)存結(jié)構(gòu)一樣真實存在这敬,而是一個抽象的概念。
?
從抽象的角度來看蕉朵,JMM 定義了線程和主內(nèi)存之間的抽象關(guān)系:
- 線程之間的共享變量存儲在主內(nèi)存(Main Memory)中
- 每個線程都有一個私有的本地內(nèi)存(Local Memory)鹅颊,本地內(nèi)存是 JMM 的一個抽象概念,并不真實存在墓造,它涵蓋了緩存堪伍、寫緩沖區(qū)、寄存器以及其他的硬件和編譯器優(yōu)化觅闽。本地內(nèi)存中存儲了該線程以讀/寫共享變量的拷貝副本帝雇。
- 從更低的層次來說,主內(nèi)存就是硬件的內(nèi)存蛉拙,而為了獲取更好的運行速度尸闸,虛擬機及硬件系統(tǒng)可能會讓工作內(nèi)存優(yōu)先存儲于寄存器和高速緩存中。
- Java 內(nèi)存模型中的線程的工作內(nèi)存(working memory)是 cpu 的寄存器和高速緩存的抽象描述孕锄。而 JVM 的靜態(tài)內(nèi)存儲模型(JVM 內(nèi)存模型)只是一種對內(nèi)存的物理劃分而已吮廉,它只局限在內(nèi)存,而且只局限在 JVM 的內(nèi)存畸肆。
分析上圖內(nèi)容可知:線程 A/B 之間通信主要經(jīng)歷步驟為:
- 線程 A 把本地內(nèi)存 A 中更新過的變量刷新到主內(nèi)存中
- 線程 B 讀取主內(nèi)存中 A 刷新過后的共享變量
從整體上看宦芦,這個通信過程需要經(jīng)過主內(nèi)存。JMM 通過控制主內(nèi)存與每個線程本地內(nèi)存之間的交互來提供內(nèi)存可見性保證轴脐。
JMM 規(guī)定主內(nèi)存與工作內(nèi)存交互協(xié)議
關(guān)于主內(nèi)存與工作內(nèi)存之間的具體交互協(xié)議调卑,即一個變量如何從主內(nèi)存拷貝到工作內(nèi)存、如何從工作內(nèi)存同步到主內(nèi)存之間的實現(xiàn)細(xì)節(jié)大咱,JMM 定義了以下八種操作來完成:
- lock(鎖定):作用于主內(nèi)存的變量恬涧,把一個變量標(biāo)識為一條線程獨占狀態(tài)。
- unlock(解鎖):作用于主內(nèi)存變量碴巾,把一個處于鎖定狀態(tài)的變量釋放出來溯捆,釋放后的變量才可以被其他線程鎖定。
- read(讀认闷啊):作用于主內(nèi)存變量提揍,把一個變量值從主內(nèi)存?zhèn)鬏數(shù)骄€程的工作內(nèi)存中,以便隨后的 load 動作使用
- load(裝載):作用于工作內(nèi)存的變量旷痕,它把 read 操作從主內(nèi)存中得到的變量值放入工作內(nèi)存的變量副本中碳锈。
- use(使用):作用于工作內(nèi)存的變量,把工作內(nèi)存中的一個變量值傳遞給執(zhí)行引擎欺抗,每當(dāng)虛擬機遇到一個需要使用變量的值的字節(jié)碼指令時將會執(zhí)行這個操作售碳。
- assign(賦值):作用于工作內(nèi)存的變量,它把一個從執(zhí)行引擎接收到的值賦值給工作內(nèi)存的變量,每當(dāng)虛擬機遇到一個給變量賦值的字節(jié)碼指令時執(zhí)行這個操作贸人。
- store(存儲):作用于工作內(nèi)存的變量间景,把工作內(nèi)存中的一個變量的值傳送到主內(nèi)存中,以便隨后的 write 的操作艺智。
- write(寫入):作用于主內(nèi)存的變量倘要,它把 store 操作從工作內(nèi)存中一個變量的值傳送到主內(nèi)存的變量中。
JMM 還規(guī)定了在執(zhí)行上述八種基本操作時十拣,必須滿足如下規(guī)則:
- 如果要把一個變量從主內(nèi)存中復(fù)制到工作內(nèi)存封拧,就需要按順尋地執(zhí)行 read 和 load 操作, 如果把變量從工作內(nèi)存中同步回主內(nèi)存中夭问,就要按順序地執(zhí)行 store 和 write 操作泽西。但 JMM 只要求上述操作必須按順序執(zhí)行,而沒有保證必須是連續(xù)執(zhí)行缰趋。
- 不允許 read 和 load捧杉、store 和 write 操作之一單獨出現(xiàn)
- 不允許一個線程丟棄它的最近 assign 的操作,即變量在工作內(nèi)存中改變了之后必須同步到主內(nèi)存中秘血。
- 不允許一個線程無原因地(沒有發(fā)生過任何 assign 操作)把數(shù)據(jù)從工作內(nèi)存同步回主內(nèi)存中味抖。
- 一個新的變量只能在主內(nèi)存中誕生,不允許在工作內(nèi)存中直接使用一個未被初始化(load 或 assign)的變量灰粮。即就是對一個變量實施 use 和 store 操作之前仔涩,必須先執(zhí)行過了 assign 和 load 操作。
- 一個變量在同一時刻只允許一條線程對其進(jìn)行 lock 操作谋竖,但 lock 操作可以被同一條線程重復(fù)執(zhí)行多次红柱,多次執(zhí)行 lock 后,只有執(zhí)行相同次數(shù)的 unlock 操作蓖乘,變量才會被解鎖。lock 和 unlock 必須成對出現(xiàn)
- 如果對一個變量執(zhí)行 lock 操作韧骗,將會清空工作內(nèi)存中此變量的值嘉抒,在執(zhí)行引擎使用這個變量前需要重新執(zhí)行 load 或 assign 操作初始化變量的值
- 如果一個變量事先沒有被 lock 操作鎖定,則不允許對它執(zhí)行 unlock 操作袍暴;也不允許去 unlock 一個被其他線程鎖定的變量些侍。
- 對一個變量執(zhí)行 unlock 操作之前,必須先把此變量同步到主內(nèi)存中(執(zhí)行 store 和 write 操作)政模。
重排序
為什么要重排序岗宣?
為了提高性能,編譯器與處理器通常會對指令做重排序淋样,通常為 3 種
- 編譯器優(yōu)化的重排序耗式,不改變單線程語義的情況下重新安排語句執(zhí)行順序。
- 指令級并行的重排序,現(xiàn)在處理器采用指令并行技術(shù)刊咳,可將多條指令并行執(zhí)行彪见。如果不存在數(shù)據(jù)依賴性,可以改變語句對應(yīng)指令順序娱挨。
- 內(nèi)存系統(tǒng)的重排序余指,由于處理器使用緩存和讀寫緩存區(qū),使得加載和存儲操作看上去是亂序執(zhí)行跷坝。
源碼如何變成執(zhí)行指令酵镜?
步驟:源代碼->1 編譯器優(yōu)化重排序->2 指令級并行重排序->3 內(nèi)存系統(tǒng)重排序-> 最終執(zhí)行指令
對于步驟 1 是編譯器重排序,步驟 2柴钻、3 是處理器重排序淮韭。
- 對于編譯器重排序,JMM 的編譯器重排序規(guī)則會禁止特定類型的重排序
- 對于處理器重排序顿颅,JMM 的處理器重排序規(guī)則會要求 Java 編譯器生成指令序列時插入特定類型的內(nèi)存屏障指令來禁止特定類型的重排序缸濒。
內(nèi)存屏障
- load(裝載):作用于工作內(nèi)存的變量,它把 read 操作從主內(nèi)存中得到的變量值放入工作內(nèi)存的變量副本中粱腻。
- store(存儲):作用于工作內(nèi)存的變量庇配,把工作內(nèi)存中的一個變量的值傳送到主內(nèi)存中,以便隨后的 write 的操作绍些。
內(nèi)存屏障的四種類型如下:
屏障類型 | 指令示例 | 說明 |
---|---|---|
LoadLoad 屏障 | Load1;LoadLoad;Load2 | 確保 Load1 數(shù)據(jù)裝載先于 Load2 及所有后續(xù)裝載指令的裝載 |
StoreStore 屏障 | Store1;StoreStore;Store2 | 確保 Store1 數(shù)據(jù)對其他處理器可見(刷新到內(nèi)存)先于 Store2 及所有后續(xù)存儲指令的存儲 |
LoadStore 屏障 | Load1;LoadStore;Store2 | 確保 Load1 數(shù)據(jù)裝載先于 Store2 及所有后續(xù)存儲指令刷新到內(nèi)存 |
StoreLoad 屏障 | Store1;StoreLoad;Load2 | 確保 Store1 數(shù)據(jù)對其他處理器可見(刷新到內(nèi)存)先于 Load2 及所有后續(xù)裝載指令的裝載捞慌。該屏障會使之前所有的內(nèi)存訪問指令(存儲和裝載)完成之后,才執(zhí)行該屏障之后的內(nèi)存訪問指令 |
happens-before 語義
從 JDK5 開始柬批,Java 使用新的 JSR-133 內(nèi)存模型進(jìn)行管理啸澡。JSR-133 使用 happens-before 概念來闡述操作之間的可見性。
?
在 JMM 中如果一個操作執(zhí)行的結(jié)果需要對另外一個操作可見氮帐,那么這兩個操作之間必須要存在 happens-before 關(guān)系(2 個操作可以是同一線程或不同線程中)嗅虏。
?
JMM 把 happens-before 重排序分為 2 類:
- 會改變程序結(jié)果的重排序,JMM 要求編譯器和處理器禁止這種重排序上沐。
- 不會改變程序結(jié)果的重排序皮服,JMM 允許這種重排序。
?
分析可知 JMM 遵循一個基本原則:只要不改變程序執(zhí)行結(jié)果(單線程和同步的多線程)編譯器和處理器怎么優(yōu)化都可以参咙,比如:
- 一個鎖只被單線程訪問龄广,那么鎖可以消除
- 一個 volatile 變量只被單線程訪問,編譯器可以把它當(dāng)做普通變量使用
?
happens-before 規(guī)則:
- 程序順序規(guī)則:在一個線程內(nèi)蕴侧,按照程序代碼的順序择同,前面的代碼運行的結(jié)果能被后面的代碼可見
- 監(jiān)視器鎖規(guī)則:一個鎖的解鎖 happens-before 于后續(xù)對這個鎖的加鎖
- volatile 變量規(guī)則:對一個 volatile 域的寫,happens-before 于任意后續(xù)對這個 volatile 域的讀
- 傳遞性規(guī)則:如果 A happens-before B净宵,且 B happens-before C敲才,那么 A happens-before C
- start() 規(guī)則:指的是主線程 A 啟動子線程 B 后裹纳,子線程 B 能看到主線程在啟動線程 B 前的任何操作
- join() 規(guī)則:主線程 A 等待子線程 B 完成 (對 B 線程 join() 調(diào)用),當(dāng)子線程 B 操作完成后归斤,主線程 A 能看到 B 線程的操作
- interrupt() 規(guī)則:線程 A 調(diào)用線程 B 的 interrupt() 方法痊夭,happens-before 于線程 B 檢測中斷事件 (也就是 Thread.interrupted() 方法)
- finalize() 規(guī)則:對象的構(gòu)造函數(shù)執(zhí)行、結(jié)束 happens-before 于 finalize() 方法的開始
as-if-serial 語義
不管怎么重排序(編譯器和處理器為了提高并行度)脏里,(單線程)程序的執(zhí)行結(jié)果不會改變她我。編譯器、runtime 和處理器都必須遵守 as-if-serial 語義迫横。
?
為了遵守 as-if-serial 語義番舆,編譯器和處理器不會對存在數(shù)據(jù)依賴關(guān)系的操作做重排序,因為這種重排序會改變執(zhí)行結(jié)果矾踱。但是恨狈,如果操作之間不存在數(shù)據(jù)依賴關(guān)系,這些操作就可能被編譯器和處理器重排序呛讲。
思考
- 為什么要有 JMM禾怠? 為了在不同處理器下正確并發(fā)執(zhí)行,JMM 提出一系列規(guī)范約束各個處理器的指令執(zhí)行達(dá)到正確并發(fā)效果贝搁,主要為多線程下原子性吗氏、可見性、有序性問題
- 為什么重排序雷逆? 為了提高執(zhí)行效率弦讽,把程序執(zhí)行指令重排序。重排序在多線程情況下可能導(dǎo)致程序執(zhí)行錯誤
- 重排序可能導(dǎo)致程序執(zhí)行錯誤怎么解決膀哲? JMM 提出 happens-before 語義規(guī)則往产,約束重排序規(guī)則
- 如何約束重排序規(guī)則? 插入內(nèi)存屏障指令告訴處理器如何正確的重排序
參考
本篇文章由一文多發(fā)平臺ArtiPub自動發(fā)布