Java內(nèi)存模型

1. 引入

  • 為何需要定義Java內(nèi)存模型?使用之前的JVM內(nèi)存結(jié)構(gòu)不是已經(jīng)夠了嗎?
  • 答:目前計(jì)算機(jī)硬件都會(huì)為了平衡CPU計(jì)算速度和讀取內(nèi)存IO速度,而設(shè)計(jì)出寄存器和高速緩存。JVM為了能夠使用到這些技術(shù)(不管具體硬件)就需要設(shè)計(jì)出一套Java內(nèi)存模型出來(lái)。

2. 具體模型

image.png
  1. 所有變量都存儲(chǔ)與主內(nèi)存中。
  2. 每個(gè)線程擁有自己的工作內(nèi)存,保存了該線程使用到的變量的主內(nèi)存副本。
  3. 線程對(duì)變量的操作(讀取和修改)都要通過(guò)工作內(nèi)存實(shí)現(xiàn)。
  4. 線程之間的工作內(nèi)存不連通,通信通過(guò)主內(nèi)存實(shí)現(xiàn)。

這里引發(fā)幾個(gè)問(wèn)題:

  1. 顯然工作內(nèi)存一般位于寄存器和高速緩存上,那為何以線程為單位?不應(yīng)該是以CPU為單位嗎,因?yàn)楣ぷ鲀?nèi)存存在的目的就是為了平衡CPU速度和主內(nèi)存的速度?
    答:顯然在編程語(yǔ)言層面上不可能定義以具體CPU為單位的內(nèi)存。因?yàn)榭偛荒躂VM上來(lái)先判斷當(dāng)前系統(tǒng)有幾個(gè)CPU,然后怎么樣,且內(nèi)存的使用者是程序,程序的運(yùn)行方式是線程,且知道一個(gè)CPU同一時(shí)刻只能一個(gè)線程在運(yùn)行。所以這里可以定義線程級(jí)別的是沒(méi)問(wèn)題的。
  2. 引入工作內(nèi)存所帶來(lái)的問(wèn)題:讀取/修改變量可能會(huì)經(jīng)歷好幾步原子性操作,變成了是非原子性操作,引發(fā)的數(shù)據(jù)同步問(wèn)題。
    這里注意:我們知道工作內(nèi)存是線程為單位的,顯然數(shù)據(jù)同步問(wèn)題的關(guān)注點(diǎn)在于實(shí)例對(duì)象和靜態(tài)變量等線程共享的數(shù)據(jù)上。局部變量是沒(méi)有同步安全問(wèn)題的。
  3. 注意這里說(shuō)的主內(nèi)存和工作內(nèi)存是不同緯度上的劃分,他們之間并沒(méi)有任何關(guān)系。

3. 內(nèi)存間的交互操作

JVM定義了8種原子操作。

1、lock(鎖定):作用于主內(nèi)存的變量,它把一個(gè)變量標(biāo)示為一條線程獨(dú)占的狀態(tài)。
2、unlock(解鎖):作用于主內(nèi)存的變量,它把一個(gè)處于鎖定狀態(tài)的變量釋放出來(lái),釋放后的變量才可以被其他線程鎖定。
3、read(讀取):作用于主內(nèi)存的變量,它把一個(gè)變量的值從主內(nèi)存?zhèn)鬏數(shù)焦ぷ鲀?nèi)存中,以便隨后的load動(dòng)作使用。
4、load(載入):作用于工作內(nèi)存的變量,它把read操作從主內(nèi)存中得到的變量值放入工作內(nèi)存的變量副本中。
5、use(使用):作用于工作內(nèi)存的變量,它把工作內(nèi)存中的一個(gè)變量的值傳遞給執(zhí)行引擎,每當(dāng)虛擬機(jī)遇到一個(gè)需要使用到變量的值得字節(jié)碼指令時(shí)將會(huì)執(zhí)行這個(gè)操作。
6、assign(賦值):作用于工作內(nèi)存的變量,它把一個(gè)從執(zhí)行引擎接收到的值賦給工作內(nèi)存的變量,每當(dāng)虛擬機(jī)遇到一個(gè)給變量賦值的字節(jié)碼指令時(shí)執(zhí)行這個(gè)操作。
7、store(存儲(chǔ)):作用于工作內(nèi)存的變量,它把工作內(nèi)存中的一個(gè)變量的值傳遞到主內(nèi)存中,以便隨后的write操作使用。
8、write(寫(xiě)入):作用于主內(nèi)存的變量,它把store操作從工作內(nèi)存中得到的變量值放入主內(nèi)存的變量中。

從主內(nèi)存中讀取變量還是將工作內(nèi)存的變量寫(xiě)回主內(nèi)存等等一系列操作都是靠這8個(gè)原子完成的。了解一下就好,不需要死記硬背。

4. volatile關(guān)鍵字

總的說(shuō)來(lái)被volatile修飾的變量有兩個(gè)特性:

  1. 內(nèi)存可見(jiàn)性
  2. 禁止指令重排
1. 內(nèi)存可見(jiàn)性
  • 表現(xiàn)上
    一個(gè)線程修改了volatile變量,其他線程可以立刻得到最新值。不會(huì)存在其他線程還讀取舊值的情況。
  • 內(nèi)部實(shí)現(xiàn)上
    JVM會(huì)在對(duì)volatile變量賦值語(yǔ)句后額外生成一條lock指令。它的作用就是將工作內(nèi)存中的值寫(xiě)回主內(nèi)存,并另其他線程的工作內(nèi)存失效。這樣其他線程讀取的時(shí)候就會(huì)從主內(nèi)存中撈到最新值。
  • 可見(jiàn)性不等于線程安全
    這個(gè)應(yīng)該很容易理解。比如c是volatile的,但c++這個(gè)操作顯然不是原子操作,不是原子操作就不會(huì)保證線程安全。
2. 禁止指令重排

JVM會(huì)對(duì)字節(jié)碼生成的匯編指令進(jìn)行指令重排(目前計(jì)算機(jī)處理器都會(huì)這么做來(lái)提高執(zhí)行效率。但會(huì)保證最終結(jié)果的正確性。即指令執(zhí)行順序可以打亂,但不會(huì)影響代碼的結(jié)果正確性。JVM也吸取了這個(gè)特性)。

  • 表現(xiàn)上
    被volatile修飾的變量,當(dāng)執(zhí)行到賦值操作時(shí),可以保證上面的代碼都已執(zhí)行結(jié)束。包括賦值操作本身。
    例如:
  • 內(nèi)部實(shí)現(xiàn)上
    還是那個(gè)lock指令。我們知道JVM會(huì)在賦值語(yǔ)句后生成lock指令。它的作用相當(dāng)于一個(gè)內(nèi)存屏障,指令重排不可能將屏障后的指令放到屏障前面。
    這里為何相當(dāng)于
    這里存在兩種說(shuō)法:禁止指令重排JVM有兩種實(shí)現(xiàn)方式:lock指令和內(nèi)存屏障。這個(gè)我們后續(xù)有時(shí)間再研究。
  • 經(jīng)典例子
    禁止指令重排的經(jīng)典例子就是DCL(雙鎖檢測(cè))方式的單例模式。
public class Singleton {
    private volatile static Singleton singleton = null;
    private Singleton(){}//禁止使用構(gòu)造函數(shù)來(lái)實(shí)例化對(duì)象
    public static Singleton getSingletonInstance(){
        if (singleton == null) {
            synchronized (Singleton.class) {
                if (singleton == null) {
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }
}

這里我們解釋一下如果singleton變量不加volatile將會(huì)發(fā)生什么?
我們看一下這一條語(yǔ)句:

singleton = new Singleton();

其實(shí)這條語(yǔ)句會(huì)生成三條指令:

(1) memory = allocate();//申請(qǐng)內(nèi)存
(2) cotrInstance(memeory);//實(shí)例化內(nèi)存對(duì)象
(3) singleton = memory;//將singleton指向內(nèi)存
  • 第2步依賴第1步,所以1和2不會(huì)重排。但是3和2可能會(huì)發(fā)生重排。即singleton指向了一塊內(nèi)存,此時(shí)這塊內(nèi)存還沒(méi)初始化結(jié)束。
  • 所以如果此時(shí)線程1執(zhí)行到了1->3的時(shí)候,線程2訪問(wèn)getSingletonInstance函數(shù),最外層的if的時(shí)候,發(fā)現(xiàn)singleton!=null,然后就會(huì)返回singleton。但此時(shí)singleton其實(shí)并沒(méi)有初始化結(jié)束,所以返回的是構(gòu)造不完全的對(duì)象。
  • 有了volatile修飾之后,就會(huì)禁止指令排序,即(3)執(zhí)行的時(shí)候,(1)和(2)已經(jīng)執(zhí)行完成。這也是jdk1.5之前無(wú)法使用DCL這種方式寫(xiě)單例模式的原因。因?yàn)閖dk1.5之前volatile仍不完全避免指令重排。

5. 對(duì)long和double型變量的特殊規(guī)則

Java內(nèi)存模型定義了一個(gè)規(guī)定:對(duì)于64位數(shù)據(jù)(long和double),允許虛擬機(jī)將沒(méi)有被volatile修飾的64位數(shù)據(jù)的讀取操作分為兩次32位操作來(lái)進(jìn)行。

  • 這個(gè)規(guī)定有點(diǎn)坑,意思就是如果多個(gè)縣城共享一個(gè)未聲明volatile的long或double變量,則可能會(huì)讀取一個(gè)既非原值也不是其他線程修飾值的“半個(gè)變量”數(shù)值。
  • 但目前商用的Java虛擬機(jī)不會(huì)這么做。還是把long和double變量的修改變成原子操作。

6. 原子性、可見(jiàn)性和有序性

1. 原子性

我們知道java內(nèi)存模型中的8種原子操作都是原子性的。
加互斥鎖(比如synchronized)可以保證一段代碼是原子性的。

2. 可見(jiàn)性

synchronized、final、volatile都可以保證可見(jiàn)性。
說(shuō)一下final為何可以?
在對(duì)象初始化完之后,對(duì)象的狀態(tài)都不可變了,自然保證了線程的可見(jiàn)性。

3. 有序性

synchronized、volatile都可以保證可見(jiàn)性。
volatile就不說(shuō)了,synchronized保證了一段代碼同一個(gè)時(shí)間只有一個(gè)線程訪問(wèn),自然不會(huì)出現(xiàn)指令重排造成的問(wèn)題。指令重排造成的問(wèn)題只會(huì)在多線程下存在。

7. 先行發(fā)生原則

Java內(nèi)存模型定義了一些默認(rèn)的偏序規(guī)則,即這些規(guī)則無(wú)需任何顯式同步代碼來(lái)實(shí)現(xiàn)。所以不滿足如下規(guī)則的且沒(méi)有同步代碼實(shí)現(xiàn)的都可能會(huì)被重排。

1、程序次序規(guī)則。在一個(gè)線程內(nèi),書(shū)寫(xiě)在前面的代碼先行發(fā)生于后面的。確切地說(shuō)應(yīng)該是,按照程序的控制流順序,因?yàn)榇嬖谝恍┓种ЫY(jié)構(gòu)。
2、Volatile變量規(guī)則。對(duì)一個(gè)volatile修飾的變量,對(duì)他的寫(xiě)操作先行發(fā)生于讀操作。
3、線程啟動(dòng)規(guī)則。Thread對(duì)象的start()方法先行發(fā)生于此線程的每一個(gè)動(dòng)作。
4、線程終止規(guī)則。線程的所有操作都先行發(fā)生于對(duì)此線程的終止檢測(cè)。
5、線程中斷規(guī)則。對(duì)線程interrupt()方法的調(diào)用先行發(fā)生于被中斷線程的代碼所檢測(cè)到的中斷事件。
6、對(duì)象終止規(guī)則。一個(gè)對(duì)象的初始化完成(構(gòu)造函數(shù)之行結(jié)束)先行發(fā)生于發(fā)的finilize()方法的開(kāi)始。
7、傳遞性。A先行發(fā)生B,B先行發(fā)生C,那么,A先行發(fā)生C。
8、管程鎖定規(guī)則。一個(gè)unlock操作先行發(fā)生于后面對(duì)同一個(gè)鎖的lock操作。

無(wú)需死記硬背,了解一下就好。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容