摘要:內(nèi)存模型指定了如何與計(jì)算機(jī)內(nèi)存協(xié)同工作。內(nèi)部的內(nèi)存模型內(nèi)存模型在內(nèi)部使用,將內(nèi)存分為了線程棧和堆。下面的圖從邏輯角度給出了內(nèi)存模型每個運(yùn)行在內(nèi)部的線程都有自己的線程棧。部分線程棧和堆可能在某些時候會占用緩存和內(nèi)部寄存器。
Java內(nèi)存模型指定了JVM如何與計(jì)算機(jī)內(nèi)存協(xié)同工作。JVM是整個計(jì)算機(jī)的模型因此這個模型包含了內(nèi)存模型,也就是Java內(nèi)存模型。
如果你像要設(shè)計(jì)正確行為的并發(fā)程序,那么了解Java內(nèi)存模型是非常重要的。Java內(nèi)存模型指定了如何以及何時不同的線程能夠看到其他線程寫入共享變量的值,以及如何在需要的時候如何同步訪問共享變量。
最初的Java內(nèi)存模型是不足的,因此Java內(nèi)存模型在Java1.5做了改進(jìn),這個版本的Java內(nèi)存模型在Java8中仍然被使用。
內(nèi)部的Java內(nèi)存模型Java內(nèi)存模型在JVM內(nèi)部使用,將內(nèi)存分為了線程棧和堆。下面的圖從邏輯角度給出了Java內(nèi)存模型:
每個運(yùn)行在JVM內(nèi)部的線程都有自己的線程棧。線程棧包含關(guān)于線程調(diào)用的哪個方法到達(dá)了當(dāng)前執(zhí)行點(diǎn)的信息。我對此引用為“調(diào)用棧”。隨著線程執(zhí)行代碼,調(diào)用棧會發(fā)生變化。
調(diào)用棧還包含每個被執(zhí)行的方法的所有本地變量(所有調(diào)用棧上的方法)。一個線程只能夠訪問它自己的線程棧。由一個線程創(chuàng)建的本地變量對其他線程不可見。即使兩個線程執(zhí)行同一段代碼,這兩個線程也會在他們各自的線程棧中創(chuàng)建這段代碼涉及的本地變量。因此,每個線程都有自己版本的本地變量。
所有內(nèi)建類型的本地變量(boolean,byte,short,char,int,long,float,double)被存儲在線程棧并且對其他線程不可見。一個線程可能會傳遞一個內(nèi)建類型變量的副本給其他線程,但是它不會貢獻(xiàn)它自己的內(nèi)建本地變量。
堆包含了你的Java程序中創(chuàng)建的所有對象,不管是哪個線程創(chuàng)建的。這包含了對象版本的內(nèi)建類型(如Byte,Integer,Long等等)。如果一個對象唄創(chuàng)建并被復(fù)制給一個本地變量,或者被創(chuàng)建為一個成員變量都是沒關(guān)系的,對象仍然存儲在堆上。
下圖給出了調(diào)用棧和存儲在線程棧中的本地變量,以及存儲在堆上的對象:
一個本地變量可能是一個內(nèi)建類型,這種情況它完全存儲在線程棧。
一個本地變量可能是一個對象的引用。這種情況這個引用(本地變量)存儲在線程棧中,但是對象本身存儲在堆上。
一個對象可能包含方法,并且這些方法可能包含本地變量。這些本地變量存儲在線程棧,即使方法所屬對象存儲在堆上。
一個對象的成員變量和對象一起存儲在堆上。對于成員變量是內(nèi)建類型,或者它是對象的引用都是如此。
靜態(tài)類變量和類定義一起存儲在堆上。
堆上的對象能夠被所有擁有這個對象引用的線程訪問。當(dāng)一個線程訪問一個對象,它也可以訪問這個對象的成員變量。如果兩個線程在同一個對象上同時調(diào)用它的同一個方法,這兩個線程會同時又權(quán)限訪問這個對象的成員變量,但是每個線程會有它自己的本地變量副本。
下面的圖給出了上面所說的:
兩個線程有同一組本地變量。一個本地變量(Local Variable 2)指向了堆上的一個共享對象(Object3)。每個線程都有對同一個對象的不同引用。它們的引用是本地變量并且存儲在各自的線程棧上,盡管這兩個不同的引用指向堆上的同一個對象。
注意共享對象(Object 3)有一個對Object2和Object4的引用作為它的成員變量,通過Object3中的這些成員變量引用,這兩個線程可以訪問Object2和Object4。
圖中還給出了一個本地變量指向堆上的兩個不同的對象。這個例子中引用指向了兩個不同對象(Object1和Object5),而不是同一個對象。理論上所有線程如果有指向所有有對象的引用,那么這些線程可以訪問到Object1和Object5。但是在圖中每個線程只有一個引用指向這兩個對象之一。
那么,什么樣的Java代碼能夠滿足上面的內(nèi)存圖示?請看下面的簡單代碼:
public class MyRunnable implements Runnable { public void run() { methodOne(); } public void methodOne() { int localVariable1 = 45; MyShareObject localVariable2 = MyShareObject.shareInstance; // ... do more with local variables. methodTwo(); } public void methodTwo() { Integer localVariable1 = new Integer(99); // ... do more with local variable. } }
public class MyShareObject { // static variable pointing to instance of MyShareObject public static final MySharedObject sharedInstance = new MySharedObject(); // member variable pointing to two objects on the heap public Integer object2 = new Integer(22); public Integer object4 = new Integer(44); public long member1 = 12345; public long member2 = 67890; }
如果兩個線程執(zhí)行run()方法,則圖中所示就是結(jié)果。run()方法調(diào)用methodOne()然后methodOne()調(diào)用methodTwo()。
methodOne()聲明了一個內(nèi)建類型的本地變量(int類型的localVariable1),另一個本地變量是一個對象的引用(localVariable2)。
每個執(zhí)行methodOne()的線程會在各自的線程棧上創(chuàng)建它自己的localVariable1和localVariable2的副本。兩個localVariable1變量完全和對方?jīng)]有關(guān)系,只是活在各自的線程棧上。一個線程不能看到另一個線程它自己的localVariable1副本變化。
每個執(zhí)行methodOne()的線程也會在各自的線程棧上創(chuàng)建它們自己的localVariable2副本。然而這兩個不同的localVariable2副本是指向堆上的同一個對象。代碼設(shè)置localVariable2指向被一個靜態(tài)變量引用的對象。這里只有一個靜態(tài)變量的副本并且這個副本存儲在堆上。因此所有l(wèi)ocalVariable2的這兩個副本都指向同一個被靜態(tài)變量指向的MySharedObject實(shí)例。MySharedObject實(shí)例存儲在堆上,它對應(yīng)圖上的Object3。
注意MySharedObject類還包含了兩個成員變量。成員變量和這個對象一起存儲在堆上。這兩個成員變量指向了兩個Integer對象。這些Integer對象對應(yīng)圖上Object2和Object4。
注意methodTwo()創(chuàng)建了一個名為localVariable1的本地變量,這個本地變量是一個Integer對象的引用。這個方法設(shè)置localVariable1引用指向了一個新的Integer實(shí)例。localVariable1引用會存儲在執(zhí)行methodTwo()方法的每個線程的副本中。兩個被實(shí)例化的Integer對象會存儲在堆中,但是由于每次方法執(zhí)行時都創(chuàng)建了一個新的Integer對象,兩個線程會執(zhí)行并創(chuàng)建兩個不同的Integer實(shí)例。methodTwo()中創(chuàng)建的Integer對象對應(yīng)圖中的Object1和Object5。
注意MySharedObject中的兩個long型的成員變量是內(nèi)建類型。由于這些變量的成員變量,因此它們?nèi)匀缓蛯ο笠黄鸫鎯υ诙焉稀V挥斜镜刈兞繒鎯υ诰€程棧上。
硬件內(nèi)存架構(gòu)現(xiàn)代硬件內(nèi)存架構(gòu)和內(nèi)部Java內(nèi)存模型有些區(qū)別。對于了解Java內(nèi)存模型如何工作,了解硬件內(nèi)存架構(gòu)也很重要。這部分描述通用硬件內(nèi)存架構(gòu),下一個部分會描述Java內(nèi)存模型是如何工作在硬件內(nèi)存之上。
這里有一個簡單的計(jì)算機(jī)硬件架構(gòu)模型:
現(xiàn)代計(jì)算機(jī)通常有2個或更多的CPU。有些CPU還有多個核。重點(diǎn)是,在一個有2個或更多CPU的計(jì)算機(jī)上,有多個線程同時運(yùn)行是可能的。每個CPU能夠在任何時候運(yùn)行一個線程。這意味著如果你的Java程序是多線程的,每個CPU一個線程同時并發(fā)運(yùn)行在你的Java程序中。
每個CPU包含一組寄存器,本質(zhì)行是CPU內(nèi)的存儲。CPU在這些寄存器中執(zhí)行操作會比在主存中快的多。這是因?yàn)镃PU能夠更快的訪問這些寄存器。
每個CPU可能還有一個CPU緩存層。實(shí)際上,大部分現(xiàn)代CPU都有一個特定大小的緩存層。CPU能比訪問主存更快的訪問緩存,但是一般不會比訪問它的內(nèi)部寄存器更快。因此,CPU緩存是一個介于內(nèi)部寄存器和主存之間的地方。有些CPU可能有多級緩存(Level1和Level2),但是這對理解Java內(nèi)存模型如何與內(nèi)存交互來說并不是很需要知道。
一個計(jì)算機(jī)也包含一個主存區(qū)域(RAM)。所有CPU都能訪問主存。主存區(qū)域比CPU緩存大的多。
一般來說,當(dāng)一個CPU需要訪問主存,它會將主存的一本讀取到它的CPU緩存。甚至它可能會讀取部分緩存到它的內(nèi)部寄存器并在其上操作。當(dāng)CPU需要將結(jié)果寫回到主存它會將值從內(nèi)部寄存器刷到緩存,在摸個時間點(diǎn)將緩存中的值刷回到主存。
當(dāng)CPU需要在緩存中存儲一些其他東西時,緩存中存儲的值會被刷回到主存。每次緩存更新時,CPU不必讀寫整塊緩存。對于緩存在較小內(nèi)存塊上的更新的標(biāo)準(zhǔn)說法是“cache lines”。一個或多個cache lines會被讀到緩存,一個或多個cache lines會被刷回主存。
連接Java內(nèi)存模型和硬件內(nèi)存架構(gòu)上面說道,Java內(nèi)存模型和硬件內(nèi)存架構(gòu)不同。硬件內(nèi)存架構(gòu)不會分辨線程棧和堆。在硬件上,線程棧和堆都定位到主存。部分線程棧和堆可能在某些時候會占用CPU緩存和內(nèi)部CPU寄存器。如下圖所示:
當(dāng)對象和變量能被存儲在計(jì)算機(jī)的不同內(nèi)存區(qū)域時,特定的問題就會發(fā)生。兩個主要問題是:
線程更新(寫)到共享變量的可見性
讀寫檢查共享變量時發(fā)生的競態(tài)條件
這些問題會在下面的部分解釋。
共享變量的可見性如果兩個或多個線程共享一個對象,如果沒有恰當(dāng)使用volatile聲明或者同步,一個線程對共享變量的更新對其他線程可能會不可見。
想象一個共享對象初始存儲在主存。一個運(yùn)行在CPU1上的線程將這個共享變量讀取到它的CPU緩存,然后對這個共享變量做一些改變,只要CPU緩存沒有被刷回主存,這個共享變量的變更版本對運(yùn)行在其他CPU上的線程就是不可見的。這種方式每個線程會有這個共享變量的本地副本,每個副本位于不同的CPU緩存中。
下圖展示了這種情況。運(yùn)行在左邊CPU的線程將共享變量拷貝到它的CPU緩存,并將這個對象的count變量變?yōu)?.這個變化對運(yùn)行在右邊CPU上的線程不可見,因?yàn)閷ount的更新還沒有刷回主存。
為了解決這個問題,你可以使用Kava的volatile關(guān)鍵字。volatile關(guān)鍵字能夠保證一個給定的變量從主存中讀取,并且當(dāng)變量更新時會寫回主存。
競態(tài)條件如果兩個或多個線程共享一個對象,多余一個線程更新這個共享對象的變量,靜態(tài)條件就可能發(fā)生。
想象如果線程A讀取了一個共享對象的count變量到它的CPU緩存,線程B做同樣的事情,但是是在一個不同的CPU緩存。現(xiàn)在線程A對count加1,線程B也對count加1.現(xiàn)在count被加了兩次,每次都是在不同的CPU緩存。
如果這些增加的操作被順序執(zhí)行,那么變量count會增加兩次并有初始值+2的值被寫回主存。
但是這兩次增加是在沒有同步的情況下并發(fā)操作的。不管線程A還是線程B將它們對count的更新版本寫回主存,count只會得到初始值+1,盡管有兩次更新。
下面的圖描述了靜態(tài)條件:
為了解決這個問題你可以用一個synchronized塊。一個synchronized塊保證了同時只有一個線程能進(jìn)入一個給定的關(guān)鍵代碼區(qū)域。synchronized塊也保證了所有在synchronized塊中訪問的變量會從主存中讀取,當(dāng)一個線程退出synchronized塊,所有對變量的更新會再次刷回主存,不管這個變量是否被聲明為volatile。
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://systransis.cn/yun/75958.html
摘要:因?yàn)楣芾砣藛T是了解手下的人員以及自己負(fù)責(zé)的事情的。處理器優(yōu)化和指令重排上面提到在在和主存之間增加緩存,在多線程場景下會存在緩存一致性問題。有沒有發(fā)現(xiàn),緩存一致性問題其實(shí)就是可見性問題。 網(wǎng)上有很多關(guān)于Java內(nèi)存模型的文章,在《深入理解Java虛擬機(jī)》和《Java并發(fā)編程的藝術(shù)》等書中也都有關(guān)于這個知識點(diǎn)的介紹。但是,很多人讀完之后還是搞不清楚,甚至有的人說自己更懵了。本文,就來整體的...
摘要:因?yàn)楣芾砣藛T是了解手下的人員以及自己負(fù)責(zé)的事情的。處理器優(yōu)化和指令重排上面提到在在和主存之間增加緩存,在多線程場景下會存在緩存一致性問題。有沒有發(fā)現(xiàn),緩存一致性問題其實(shí)就是可見性問題。 網(wǎng)上有很多關(guān)于Java內(nèi)存模型的文章,在《深入理解Java虛擬機(jī)》和《Java并發(fā)編程的藝術(shù)》等書中也都有關(guān)于這個知識點(diǎn)的介紹。但是,很多人讀完之后還是搞不清楚,甚至有的人說自己更懵了。本文,就來整體的...
摘要:編譯器,和處理器會共同確保單線程程序的執(zhí)行結(jié)果與該程序在順序一致性模型中的執(zhí)行結(jié)果相同。正確同步的多線程程序的執(zhí)行將具有順序一致性程序的執(zhí)行結(jié)果與該程序在順序一致性內(nèi)存模型中的執(zhí)行結(jié)果相同。 前情提要 深入理解Java內(nèi)存模型(六)——final 處理器內(nèi)存模型 順序一致性內(nèi)存模型是一個理論參考模型,JMM和處理器內(nèi)存模型在設(shè)計(jì)時通常會把順序一致性內(nèi)存模型作為參照。JMM和處理器內(nèi)...
摘要:內(nèi)存模型即,簡稱,其規(guī)范了虛擬機(jī)與計(jì)算機(jī)內(nèi)存時如何協(xié)同工作的,規(guī)定了一個線程如何和何時看到其他線程修改過的值,以及在必須時,如何同步訪問共享變量。內(nèi)存模型要求調(diào)用棧和本地變量存放在線程棧上,對象存放在堆上。 Java內(nèi)存模型即Java Memory Model,簡稱JMM,其規(guī)范了Java虛擬機(jī)與計(jì)算機(jī)內(nèi)存時如何協(xié)同工作的,規(guī)定了一個線程如何和何時看到其他線程修改過的值,以及在必須時,...
摘要:作為一個程序員,不了解內(nèi)存模型就不能寫出能夠充分利用內(nèi)存的代碼。程序計(jì)數(shù)器是在電腦處理器中的一個寄存器,用來指示電腦下一步要運(yùn)行的指令序列。在虛擬機(jī)中,本地方法棧和虛擬機(jī)棧是共用同一塊內(nèi)存的,不做具體區(qū)分。 作為一個 Java 程序員,不了解 Java 內(nèi)存模型就不能寫出能夠充分利用內(nèi)存的代碼。本文通過對 Java 內(nèi)存模型的介紹,讓讀者能夠了解 Java 的內(nèi)存的分配情況,適合 Ja...
閱讀 3057·2021-11-19 11:31
閱讀 3147·2021-09-02 15:15
閱讀 1001·2019-08-29 17:22
閱讀 1071·2019-08-29 16:38
閱讀 2475·2019-08-26 13:56
閱讀 844·2019-08-26 12:16
閱讀 1448·2019-08-26 11:29
閱讀 941·2019-08-26 10:12