摘要:內存模型指定了如何與計算機內存協同工作。內部的內存模型內存模型在內部使用,將內存分為了線程棧和堆。下面的圖從邏輯角度給出了內存模型每個運行在內部的線程都有自己的線程棧。部分線程棧和堆可能在某些時候會占用緩存和內部寄存器。
Java內存模型指定了JVM如何與計算機內存協同工作。JVM是整個計算機的模型因此這個模型包含了內存模型,也就是Java內存模型。
如果你像要設計正確行為的并發程序,那么了解Java內存模型是非常重要的。Java內存模型指定了如何以及何時不同的線程能夠看到其他線程寫入共享變量的值,以及如何在需要的時候如何同步訪問共享變量。
最初的Java內存模型是不足的,因此Java內存模型在Java1.5做了改進,這個版本的Java內存模型在Java8中仍然被使用。
內部的Java內存模型Java內存模型在JVM內部使用,將內存分為了線程棧和堆。下面的圖從邏輯角度給出了Java內存模型:
每個運行在JVM內部的線程都有自己的線程棧。線程棧包含關于線程調用的哪個方法到達了當前執行點的信息。我對此引用為“調用棧”。隨著線程執行代碼,調用棧會發生變化。
調用棧還包含每個被執行的方法的所有本地變量(所有調用棧上的方法)。一個線程只能夠訪問它自己的線程棧。由一個線程創建的本地變量對其他線程不可見。即使兩個線程執行同一段代碼,這兩個線程也會在他們各自的線程棧中創建這段代碼涉及的本地變量。因此,每個線程都有自己版本的本地變量。
所有內建類型的本地變量(boolean,byte,short,char,int,long,float,double)被存儲在線程棧并且對其他線程不可見。一個線程可能會傳遞一個內建類型變量的副本給其他線程,但是它不會貢獻它自己的內建本地變量。
堆包含了你的Java程序中創建的所有對象,不管是哪個線程創建的。這包含了對象版本的內建類型(如Byte,Integer,Long等等)。如果一個對象唄創建并被復制給一個本地變量,或者被創建為一個成員變量都是沒關系的,對象仍然存儲在堆上。
下圖給出了調用棧和存儲在線程棧中的本地變量,以及存儲在堆上的對象:
一個本地變量可能是一個內建類型,這種情況它完全存儲在線程棧。
一個本地變量可能是一個對象的引用。這種情況這個引用(本地變量)存儲在線程棧中,但是對象本身存儲在堆上。
一個對象可能包含方法,并且這些方法可能包含本地變量。這些本地變量存儲在線程棧,即使方法所屬對象存儲在堆上。
一個對象的成員變量和對象一起存儲在堆上。對于成員變量是內建類型,或者它是對象的引用都是如此。
靜態類變量和類定義一起存儲在堆上。
堆上的對象能夠被所有擁有這個對象引用的線程訪問。當一個線程訪問一個對象,它也可以訪問這個對象的成員變量。如果兩個線程在同一個對象上同時調用它的同一個方法,這兩個線程會同時又權限訪問這個對象的成員變量,但是每個線程會有它自己的本地變量副本。
下面的圖給出了上面所說的:
兩個線程有同一組本地變量。一個本地變量(Local Variable 2)指向了堆上的一個共享對象(Object3)。每個線程都有對同一個對象的不同引用。它們的引用是本地變量并且存儲在各自的線程棧上,盡管這兩個不同的引用指向堆上的同一個對象。
注意共享對象(Object 3)有一個對Object2和Object4的引用作為它的成員變量,通過Object3中的這些成員變量引用,這兩個線程可以訪問Object2和Object4。
圖中還給出了一個本地變量指向堆上的兩個不同的對象。這個例子中引用指向了兩個不同對象(Object1和Object5),而不是同一個對象。理論上所有線程如果有指向所有有對象的引用,那么這些線程可以訪問到Object1和Object5。但是在圖中每個線程只有一個引用指向這兩個對象之一。
那么,什么樣的Java代碼能夠滿足上面的內存圖示?請看下面的簡單代碼:
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; }
如果兩個線程執行run()方法,則圖中所示就是結果。run()方法調用methodOne()然后methodOne()調用methodTwo()。
methodOne()聲明了一個內建類型的本地變量(int類型的localVariable1),另一個本地變量是一個對象的引用(localVariable2)。
每個執行methodOne()的線程會在各自的線程棧上創建它自己的localVariable1和localVariable2的副本。兩個localVariable1變量完全和對方沒有關系,只是活在各自的線程棧上。一個線程不能看到另一個線程它自己的localVariable1副本變化。
每個執行methodOne()的線程也會在各自的線程棧上創建它們自己的localVariable2副本。然而這兩個不同的localVariable2副本是指向堆上的同一個對象。代碼設置localVariable2指向被一個靜態變量引用的對象。這里只有一個靜態變量的副本并且這個副本存儲在堆上。因此所有localVariable2的這兩個副本都指向同一個被靜態變量指向的MySharedObject實例。MySharedObject實例存儲在堆上,它對應圖上的Object3。
注意MySharedObject類還包含了兩個成員變量。成員變量和這個對象一起存儲在堆上。這兩個成員變量指向了兩個Integer對象。這些Integer對象對應圖上Object2和Object4。
注意methodTwo()創建了一個名為localVariable1的本地變量,這個本地變量是一個Integer對象的引用。這個方法設置localVariable1引用指向了一個新的Integer實例。localVariable1引用會存儲在執行methodTwo()方法的每個線程的副本中。兩個被實例化的Integer對象會存儲在堆中,但是由于每次方法執行時都創建了一個新的Integer對象,兩個線程會執行并創建兩個不同的Integer實例。methodTwo()中創建的Integer對象對應圖中的Object1和Object5。
注意MySharedObject中的兩個long型的成員變量是內建類型。由于這些變量的成員變量,因此它們仍然和對象一起存儲在堆上。只有本地變量會存儲在線程棧上。
硬件內存架構現代硬件內存架構和內部Java內存模型有些區別。對于了解Java內存模型如何工作,了解硬件內存架構也很重要。這部分描述通用硬件內存架構,下一個部分會描述Java內存模型是如何工作在硬件內存之上。
這里有一個簡單的計算機硬件架構模型:
現代計算機通常有2個或更多的CPU。有些CPU還有多個核。重點是,在一個有2個或更多CPU的計算機上,有多個線程同時運行是可能的。每個CPU能夠在任何時候運行一個線程。這意味著如果你的Java程序是多線程的,每個CPU一個線程同時并發運行在你的Java程序中。
每個CPU包含一組寄存器,本質行是CPU內的存儲。CPU在這些寄存器中執行操作會比在主存中快的多。這是因為CPU能夠更快的訪問這些寄存器。
每個CPU可能還有一個CPU緩存層。實際上,大部分現代CPU都有一個特定大小的緩存層。CPU能比訪問主存更快的訪問緩存,但是一般不會比訪問它的內部寄存器更快。因此,CPU緩存是一個介于內部寄存器和主存之間的地方。有些CPU可能有多級緩存(Level1和Level2),但是這對理解Java內存模型如何與內存交互來說并不是很需要知道。
一個計算機也包含一個主存區域(RAM)。所有CPU都能訪問主存。主存區域比CPU緩存大的多。
一般來說,當一個CPU需要訪問主存,它會將主存的一本讀取到它的CPU緩存。甚至它可能會讀取部分緩存到它的內部寄存器并在其上操作。當CPU需要將結果寫回到主存它會將值從內部寄存器刷到緩存,在摸個時間點將緩存中的值刷回到主存。
當CPU需要在緩存中存儲一些其他東西時,緩存中存儲的值會被刷回到主存。每次緩存更新時,CPU不必讀寫整塊緩存。對于緩存在較小內存塊上的更新的標準說法是“cache lines”。一個或多個cache lines會被讀到緩存,一個或多個cache lines會被刷回主存。
連接Java內存模型和硬件內存架構上面說道,Java內存模型和硬件內存架構不同。硬件內存架構不會分辨線程棧和堆。在硬件上,線程棧和堆都定位到主存。部分線程棧和堆可能在某些時候會占用CPU緩存和內部CPU寄存器。如下圖所示:
當對象和變量能被存儲在計算機的不同內存區域時,特定的問題就會發生。兩個主要問題是:
線程更新(寫)到共享變量的可見性
讀寫檢查共享變量時發生的競態條件
這些問題會在下面的部分解釋。
共享變量的可見性如果兩個或多個線程共享一個對象,如果沒有恰當使用volatile聲明或者同步,一個線程對共享變量的更新對其他線程可能會不可見。
想象一個共享對象初始存儲在主存。一個運行在CPU1上的線程將這個共享變量讀取到它的CPU緩存,然后對這個共享變量做一些改變,只要CPU緩存沒有被刷回主存,這個共享變量的變更版本對運行在其他CPU上的線程就是不可見的。這種方式每個線程會有這個共享變量的本地副本,每個副本位于不同的CPU緩存中。
下圖展示了這種情況。運行在左邊CPU的線程將共享變量拷貝到它的CPU緩存,并將這個對象的count變量變為2.這個變化對運行在右邊CPU上的線程不可見,因為對count的更新還沒有刷回主存。
為了解決這個問題,你可以使用Kava的volatile關鍵字。volatile關鍵字能夠保證一個給定的變量從主存中讀取,并且當變量更新時會寫回主存。
競態條件如果兩個或多個線程共享一個對象,多余一個線程更新這個共享對象的變量,靜態條件就可能發生。
想象如果線程A讀取了一個共享對象的count變量到它的CPU緩存,線程B做同樣的事情,但是是在一個不同的CPU緩存。現在線程A對count加1,線程B也對count加1.現在count被加了兩次,每次都是在不同的CPU緩存。
如果這些增加的操作被順序執行,那么變量count會增加兩次并有初始值+2的值被寫回主存。
但是這兩次增加是在沒有同步的情況下并發操作的。不管線程A還是線程B將它們對count的更新版本寫回主存,count只會得到初始值+1,盡管有兩次更新。
下面的圖描述了靜態條件:
為了解決這個問題你可以用一個synchronized塊。一個synchronized塊保證了同時只有一個線程能進入一個給定的關鍵代碼區域。synchronized塊也保證了所有在synchronized塊中訪問的變量會從主存中讀取,當一個線程退出synchronized塊,所有對變量的更新會再次刷回主存,不管這個變量是否被聲明為volatile。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/75958.html
摘要:因為管理人員是了解手下的人員以及自己負責的事情的。處理器優化和指令重排上面提到在在和主存之間增加緩存,在多線程場景下會存在緩存一致性問題。有沒有發現,緩存一致性問題其實就是可見性問題。 網上有很多關于Java內存模型的文章,在《深入理解Java虛擬機》和《Java并發編程的藝術》等書中也都有關于這個知識點的介紹。但是,很多人讀完之后還是搞不清楚,甚至有的人說自己更懵了。本文,就來整體的...
摘要:因為管理人員是了解手下的人員以及自己負責的事情的。處理器優化和指令重排上面提到在在和主存之間增加緩存,在多線程場景下會存在緩存一致性問題。有沒有發現,緩存一致性問題其實就是可見性問題。 網上有很多關于Java內存模型的文章,在《深入理解Java虛擬機》和《Java并發編程的藝術》等書中也都有關于這個知識點的介紹。但是,很多人讀完之后還是搞不清楚,甚至有的人說自己更懵了。本文,就來整體的...
摘要:編譯器,和處理器會共同確保單線程程序的執行結果與該程序在順序一致性模型中的執行結果相同。正確同步的多線程程序的執行將具有順序一致性程序的執行結果與該程序在順序一致性內存模型中的執行結果相同。 前情提要 深入理解Java內存模型(六)——final 處理器內存模型 順序一致性內存模型是一個理論參考模型,JMM和處理器內存模型在設計時通常會把順序一致性內存模型作為參照。JMM和處理器內...
摘要:內存模型即,簡稱,其規范了虛擬機與計算機內存時如何協同工作的,規定了一個線程如何和何時看到其他線程修改過的值,以及在必須時,如何同步訪問共享變量。內存模型要求調用棧和本地變量存放在線程棧上,對象存放在堆上。 Java內存模型即Java Memory Model,簡稱JMM,其規范了Java虛擬機與計算機內存時如何協同工作的,規定了一個線程如何和何時看到其他線程修改過的值,以及在必須時,...
摘要:作為一個程序員,不了解內存模型就不能寫出能夠充分利用內存的代碼。程序計數器是在電腦處理器中的一個寄存器,用來指示電腦下一步要運行的指令序列。在虛擬機中,本地方法棧和虛擬機棧是共用同一塊內存的,不做具體區分。 作為一個 Java 程序員,不了解 Java 內存模型就不能寫出能夠充分利用內存的代碼。本文通過對 Java 內存模型的介紹,讓讀者能夠了解 Java 的內存的分配情況,適合 Ja...
閱讀 3043·2021-11-19 11:31
閱讀 3127·2021-09-02 15:15
閱讀 984·2019-08-29 17:22
閱讀 1058·2019-08-29 16:38
閱讀 2464·2019-08-26 13:56
閱讀 832·2019-08-26 12:16
閱讀 1434·2019-08-26 11:29
閱讀 929·2019-08-26 10:12