Java 內(nèi)存模型概述
Java 內(nèi)存模型是通過各種操作來定義胶果,包括對變量的讀/寫操作,監(jiān)視器的加鎖和釋放操作斤斧,以及線程啟動和合并操作早抠。JMM為程序中所有的操作定義了一個(gè)偏序關(guān)系钦无,稱之為Happens-Before洁墙。要想保證操作B的線程能看到操作A的結(jié)果(無論A和B是否在同一個(gè)線程中執(zhí)行)哑子,那么在A和B之間必須滿足Happens-Before關(guān)系告嘲。如果兩個(gè)操作之間缺乏Happens-Before關(guān)系摧冀,那么JVM可以對它們?nèi)我獾嘏判?/strong>备燃。
Java內(nèi)存模型說明了某個(gè)線程的內(nèi)存操作在哪些情況下對于其他線程是可見的辅鲸。
平臺內(nèi)存模型和 Java 內(nèi)存模型
平臺內(nèi)存模型是指硬件內(nèi)存模型治筒,在共享內(nèi)存的多處理器體系架構(gòu)中酱床,每個(gè)處理器都擁有自己的緩存羊赵,并且定期地與主內(nèi)存進(jìn)行協(xié)調(diào),如下圖所示。為了告訴應(yīng)用程序可以從內(nèi)存系統(tǒng)中獲得怎樣的保證昧捷,平臺內(nèi)存模型定義了一些特殊指令(內(nèi)存柵欄/柵欄/內(nèi)存屏障)闲昭,當(dāng)需要共享數(shù)據(jù)時(shí),這些指令就能實(shí)現(xiàn)額外的存儲協(xié)調(diào)保證靡挥。
.
為了使Java開發(fā)人員無需關(guān)注不同平臺內(nèi)存模型的差異序矩,Java提供了自己的內(nèi)存模型(JMM)。Java內(nèi)存模型其實(shí)是定義了線程和主內(nèi)存之間的抽象關(guān)系:線程之間的共享變量存儲在主內(nèi)存(main memory)中跋破,每個(gè)線程都有一個(gè)私有的本地內(nèi)存(local memory)簸淀,本地內(nèi)存中存儲了該線程以讀/寫共享變量的副本。本地內(nèi)存是Java內(nèi)存模型的一個(gè)抽象概念毒返,并不真實(shí)存在租幕。它涵蓋了緩存,寫緩沖區(qū)拧簸,寄存器以及其他的硬件和編譯器優(yōu)化劲绪。
.
在JVM內(nèi)部,Java內(nèi)存模型把內(nèi)存分成了兩部分:線程棧區(qū)和堆區(qū)盆赤,下圖展示了Java內(nèi)存模型在JVM中的邏輯視圖:
.
JVM中運(yùn)行的每個(gè)線程都擁有自己的線程棧贾富,線程棧包含了當(dāng)前線程執(zhí)行的方法調(diào)用相關(guān)信息,我們也把它稱作調(diào)用棧(虛擬機(jī)棧牺六,本地方法棧颤枪,程序計(jì)數(shù)器)。一個(gè)線程只能讀取自己的線程棧兔乞,也就是說汇鞭,線程中的本地變量對其它線程是不可見的。即使兩個(gè)線程執(zhí)行的是同一段代碼庸追,它們也會各自在自己的線程棧中創(chuàng)建本地變量霍骄,因此,每個(gè)線程中的本地變量都會有自己的版本淡溯。
堆區(qū)(堆區(qū)读整,方法區(qū))包含了Java應(yīng)用創(chuàng)建的所有對象信息,不管對象是哪個(gè)線程創(chuàng)建的咱娶,其中的對象包括原始類型的封裝類(如Byte米间、Integer、Long等等)膘侮。不管對象是屬于一個(gè)成員變量還是方法中的本地變量屈糊,它都會被存儲在堆區(qū)。
因?yàn)镴ava內(nèi)存模型和平臺底層內(nèi)存模型之間的差異琼了,Java內(nèi)存模型中的變量和對象會存儲到平臺內(nèi)存模型的各個(gè)存儲區(qū)域:
.
多個(gè)線程對共享對象的訪問必然會面臨一些問題逻锐,其中最主要的兩個(gè)問題是:
1. 共享對象對各個(gè)線程的可見性
2. 共享對象的競爭現(xiàn)象
JVM通過在適當(dāng)?shù)奈恢蒙喜迦雰?nèi)存屏障來屏蔽Java 內(nèi)存模型與底層平臺內(nèi)存模型之間的差異夫晌,Java內(nèi)存模型并定義了一系列Happens-Before關(guān)系,用來保證某個(gè)線程的內(nèi)存操作對其他線程的可見性昧诱,并避免產(chǎn)生數(shù)據(jù)競爭晓淀。
指令重排序
在執(zhí)行程序時(shí),為了提高性能盏档,編譯器和處理器會對指令做重排序⌒钻現(xiàn)實(shí)中,沒有正確同步情況下蜈亩,各種使操作延遲或看似亂序執(zhí)行的原因懦窘,都可以歸結(jié)到重排序。編譯器和處理器進(jìn)行指令重排序時(shí)稚配,會遵守以下規(guī)律:
數(shù)據(jù)依賴性
如果兩個(gè)操作訪問同一個(gè)變量奶赠,其中一個(gè)為寫操作,此時(shí)這兩個(gè)操作之間存在依賴性药有,編譯器和處理器不會改變存在這兩個(gè)操作的執(zhí)行順序。
as-if-serial
不管怎么重排序苹丸,單線程下的執(zhí)行結(jié)果不能被改變愤惰,編譯器、runtime和處理器都必須遵守as-if-serial語義赘理。
內(nèi)存屏障(Memory Barrier )
插入一條Memory Barrier會告訴編譯器和CPU:不管什么指令都不能和這條Memory Barrier指令重排序宦言。
Memory Barrier所做的另外一件事是強(qiáng)制刷出各種CPU cache,如一個(gè)Write-Barrier(寫入屏障)將刷出所有在Barrier之前寫入 cache 的數(shù)據(jù)商模,因此奠旺,任何CPU上的線程都能讀取到這些數(shù)據(jù)的最新版本。
如果一個(gè)變量是volatile修飾的施流,JMM會在寫入這個(gè)字段之后插進(jìn)一個(gè)Write-Barrier指令响疚,并在讀這個(gè)字段之前插入一個(gè)Read-Barrier指令。這意味著瞪醋,如果寫入一個(gè)volatile變量忿晕,就可以保證:
1. 一個(gè)線程寫入變量a后,任何線程訪問該變量都會拿到最新值银受。
2. 在寫入變量a之前的寫入操作践盼,其更新的數(shù)據(jù)對于其他線程也是可見的。因?yàn)镸emory Barrier會刷出cache中的所有先前的寫入宾巍。
Happens-Before
Java 內(nèi)存模型為程序中所有的操作定義了一個(gè)偏序關(guān)系咕幻,稱之為Happens-Before。當(dāng)一個(gè)變量被多個(gè)線程讀取并且至少被一個(gè)線程寫入時(shí)顶霞,如果在讀操作和寫操作之間沒有依照 Happens-Before 來排序肄程,那么久會產(chǎn)生數(shù)據(jù)競爭問題。
在正確同步的程序中不存在數(shù)據(jù)競爭,并會表現(xiàn)出串行一致性绷耍,這意味著程序中的所有操作都會按照一種固定和全局的順序執(zhí)行吐限。Happens-Before規(guī)則包括:
程序順序規(guī)則
如果程序中操作A在操作B之前,那么線程中操作A將在B操作之前執(zhí)行
監(jiān)視器規(guī)則
在監(jiān)視器鎖上的解鎖操作褂始,必須在同一個(gè)監(jiān)視器鎖上的加鎖操作之前執(zhí)行
volatile 變量規(guī)則
對 volatile 變量的寫入操作必須在對該變量的讀操作之前執(zhí)行诸典。原子變量與 volatile 變量在讀操作和寫操作上有著相同的內(nèi)存語義
線程啟動規(guī)則
在線程上對 Thread.start 的調(diào)用必須在該線程中執(zhí)行任何操作之前執(zhí)行
線程結(jié)束規(guī)則
線程中的任何操作都必須在其他線程檢測到該線程已經(jīng)結(jié)束之前執(zhí)行
中斷規(guī)則規(guī)則
當(dāng)一個(gè)線程在另一個(gè)線程上調(diào)用 interrupt 時(shí),必須在被中斷線程檢測到 interrupt調(diào)用之前執(zhí)行
終結(jié)器規(guī)則
對象的構(gòu)造函數(shù)必須在啟動該對象的終結(jié)器之前執(zhí)行完成
傳遞性規(guī)則
如果操作A在操作B之前執(zhí)行崎苗,并且操作B在操作C之前執(zhí)行狐粱,那么操作A必須在操作C之前執(zhí)行
注意:兩個(gè)操作之間具有happens-before關(guān)系,并不意味前一個(gè)操作必須要在后一個(gè)操作之前執(zhí)行胆数!僅僅要求前一個(gè)操作的執(zhí)行結(jié)果肌蜻,對于后一個(gè)操作是可見的,且前一個(gè)操作按順序排在后一個(gè)操作之前必尼。
內(nèi)容來源
Java 并發(fā)編程實(shí)戰(zhàn)
http://blog.csdn.net/suifeng3051/article/details/52611310