java线程内存模型_深度解析Java多线程的内存模型
內部java內存模型
硬件層面的內存模型
Java內存模型和硬件內存模型的聯系
共享對象的可見性
資源競速
Java內存模型很好的說明了JVM是如何在內存里工作的,JVM可以理解為java執行的一個操作系統,作為一個操作系統就有內存模型,這就是我們常說的JAVA內存模型。
如果我們想正確的寫多線程的并行程序。理解好java內存模型在多線程下的工作方式是及其重要的,這可以幫我們更好的理解底層的工作方式。
java內存模型說明了不同的線程怎樣以及何時可以看到其他線程寫入共享變量的值,以及同步程序怎么共享變量。最初的java內存模型不夠好,存在很多的不足,所以在java1.5z中,java內存模型的版本的進行了一次重大的更新與改進,并且在java8中仍然被使用。
內部java內存模型
JVM的內部的內存模型分為了兩部分,thread stack和heap,也就是線程棧和堆,我們將復雜的內存模型抽象成下圖:
Paste_Image.png
每一個在JVM中運行的線程在內存里都會有屬于自己的線程棧。線程棧一般包含這個線程的方法執行到哪一個點了這些信息,也被稱作“call stack”,當線程執行代碼,調用棧就會隨著執行的狀態改變。
線程棧也包括了每個方法執行時的local 變量,所有的方法也都存儲在線程棧上,一個線程可以只能訪問自己的線程棧。每個線程自己創建的本地本地變量對其他線程是不可見的,也就是私有的,即使兩個線程調用的是同一個方法,每個線程會分別保存一份本地變量,各自屬于各自的線程棧。
所有基本類型的local變量( boolean, byte, short, char, int, long, float, double)全都被存儲在線程棧里,而且對其他線程是不可見的,一個線程可能會傳遞一份基本類型的變量值的一份拷貝給另一個線程,但是自己本身的變量是不能共享的,只能傳遞拷貝。
堆中存儲著java程序中new出來的對象,不管是哪個線程new出來的對象,都存在一起,而且不區分是哪個線程的對象。這些對象里面也包括那些原始類型的對象版本(e.g. Byte, Integer, Long etc.). 不管這個對象是分配給本地變量還是成員變量,最終都是存在堆里。
下面這個圖就說明了線程棧中存儲了local變量,堆中存儲著對象object。
Paste_Image.png
一個原始數據類型的本地變量將完全被存儲在線程棧中。
本地變量也可以是指向對象的引用,在這種情況下,本地變量存在線程棧上,但是對象本身是存在堆上。
一個對象可能包含方法這些方法同時也會包含本地變量,這些本地變量也是存儲在線程棧上面,即使他們所屬于的對象和方法是存在堆上的。
一個對象的成員變量是跟隨著對象本身存儲在堆上的,不管成員變量是原始數據類型還是指向對象的引用。
靜態的類變量一般也存儲在堆上,根據類的定義。
存儲在堆上的對象可以被所有的線程通過引用來訪問。當一個線程持有一個對象的引用時,他同時也就可以訪問這個對象的成員變量了。如果兩個線程同時調用同一個對象的一個方法,他們就會都擁有這個對象的成員變量,但是每一個線程會享有自己私有的本地變量。
下面這張圖就說明以上的內容
Paste_Image.png
兩個線程有一系列的本地變量。其中一個本地變量(Local Variable 2)指向堆中的object3.這兩個線程每個都有指向同一個對象object3的不同引用。他們的引用是本地變量,都存在各自的線程棧中,雖然這兩個不同的引用是指向同一個對象的。
我們還可以發現,共有的對象object3有指向object2和object4的引用,這些引用是作為object3中的成員變量存在的。通過object3中的成員變量的引用,兩個線程都可以訪問到object2和object4.
這個圖也說明了指向堆中不同對象的本地變量。例如圖中的object1和object5,不是同一個對象。理論上,所有的線程都可以訪問堆中的對象,只要這個線程持有堆中對象的引用。但是這個圖中,每個線程只有這兩個對象中的一個引用。
下面,我們將寫一段實際的代碼,這段代碼的內存模型就跟上圖一樣:
public class MyRunnable implements Runnable(){
public void run() {
methodOne();
}
public void methodOne() {
int localVariable1 = 45;
MySharedObject localVariable2 =
MySharedObject.sharedInstance;
//... do more with local variables.
methodTwo();
}
public void methodTwo() {
Integer localVariable1 = new Integer(99);
//... do more with local variable.
}
}
public class MySharedObject{
//static variable pointing to instance of MySharedObject
public static final MySharedObject sharedInstance =
new MySharedObject();
//member variables 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 member1 = 67890;
}
如果兩個線程執行run方法那么,上圖的內存模型就會是這段程序的執行結果。
methodOne()聲明了一個原始數據類型的本地變量,int類型的localVariable1 ,和一個指向對象的引用的本地變量(localVariable2).
每個線程執行methodOne()的時候都會創建屬于自己的一份本地變量的拷貝,也就是
localVariable1 and localVariable2在他們各自的線程棧的空間中。localVariable1將會被完全對其他線程是不可見的,只存在與每個線程自己的線程棧空間中。一個線程不能看到其他線程對localVariable1所做的改變與操作,是不可見的。
每個線程執行methodOne()方法的時候也會創建localVariable2的拷貝,但是不同的localVariable2的拷貝最終卻指向同一個堆上的對象。這段代碼讓localVariable2指向之前通過一個靜態變量引用的對象。靜態變量只會存在一份,不會有多余的拷貝,而且靜態變量是存在堆中的。所以,localVariable2的兩份拷貝同時指向同一個MySharedObject對象的實例,與此同時,還有一個堆中的靜態變量也指向這個對象實例。這個對象就是對應上圖中的object3.
我們發現MySharedObject 包含這兩個成員變量。這些成員變量跟對象一樣存儲在堆上。這兩個成員變量指向兩個integer對象。這兩個對象分別對應上圖中的object2和object4.
我們發現,methodTwo() 創建了一個本地變量叫做localVariable1。這個本地變量是一個對象的引用,他指向一個integer對象。這個方法將本地變量localVariable1指向一個新的值。在執行methodTwo()的時候,每個線程都會持有一份localVariable1的拷貝。這兩個Integer對象將會被初始化在堆上,但是因為每次執行這個方法的時候,這個方法都會創建一個新的對象,所以兩個線程會擁有獨立的對象實例。這兩個對象就對應上圖中的object1和object5.
我們發現MySharedObject 中的成員變量是原始數據類型,但由于他們是成員變量,所以依舊存儲在堆上。只有本地變量存儲在線程棧中。
硬件層面的內存模型
硬件層面的內存內存結構與JVM中的內存結構是有不同的,對我們來說,正確理解掌握硬件層面的內存模型是很必要的,這可以幫助我們理解java多線程的底層機制,更要了解java內存模型如何在硬件內存結構上工作。這一章將講述硬件層面內存模型,下一部分將講述java如何結合硬件工作。
下圖是一個簡化的現代計算機硬件結構圖:
Paste_Image.png
現代計算機通常會有兩個甚至更多的cpu,這些cpu可能還會有多個核心,這個意義是,擁有多個cpu的計算機可能會有多個線程在同時執行,每個cpu都可以在任何給定的時間運行一個線程。這就意味著如果我們的java程序是多線程的,在內部就每個線程就會有一個cpu在同時執行。
每個cpu都會有一系列的寄存器registers在cpu的內存中,而且這些寄存器是很重要的。cpu在寄存器上進行計算操作比在主內存中進行計算要快的多。這是因為cpu訪問寄存器的速度比訪問內存要快得多。
每個cpu也會有一個cpu的cache內存。這是因為cpu訪問cache比訪問內存的速度要快得多,但是卻比訪問的寄存器要慢一些,所以cache的速度是介于寄存器和內存的。一些cpu還有多級cache,比如(Level 1 and Level 2),但是這對于我們理解java內存模型關系不大,我們只需要cpu有三層內存結構,寄存器-cache-內存(RAM).
一臺計算機一般都會有主內存也就是RAM,所有cpu都可以訪問主內存,主內存的容量一般遠比cache大得多。
一般的,當cpu需要訪問內存的時候,他會先讀取一部分主內存到cache中,甚至,會讀取一部分cache到內部的寄存器中,然后再在寄存器進行計算操作。當cpu將計算結果寫回內存中時,他會flush寄存器和cache中的數據,然后將值寫回至內存中。
當cpu要求cache去存儲其他內容時,也會將cache中的內容flush到內存中。cpu的cache可以邊寫入一部分數據到內存,邊寫入一部分到自己cache中,所以在更新數據,不必要全部清空cache,可以邊讀邊寫。一般的,cache真正更新數據是在更小的內存塊上,叫做“cache lines”。多個“cache lines”可能正在讀取數據到cache中,而另一部分可能正在將數據寫回到內存中。
Java內存模型和硬件內存模型的聯系
上文已經提到,java內存模型和硬件內存模型是不同的。硬件內存模型不區分堆和棧。在硬件層面,所有的線程棧和堆都被存儲在主內存中,一部分線程棧和堆可能有時候會出現在cpu cache中和cpu寄存器中。下圖可以說明這個問題:
Paste_Image.png
當對象和變量被存儲在不同的內存區域的時候,很多問題就可能發生,主要有以下兩類問題:
當線程對一些共享數據進行更新或者寫操作時,可見性的問題
當讀寫共享數據產生資源競速的問題
接下來的部分就會討論這兩個問題
共享對象的可見性
如果多個線程在共享一個對象,沒有正確使用volatile或者synchronize聲明,更新共享對象的時候就可能出現其他線程不可見的問題。
我們假設共享對象初始化主內存中。一個在cpu中運行的線程讀取共享對象到cache中。這時候,隨著程序的執行,可能導致共享對象發生一些變化。只要cpu的cache還沒有被寫回到主內存中,這個共享對象的變化就對其他在cpu上運行的線程不可見。這種情況下,每個線程都會有持有一份自己對于共享對象的拷貝,這份拷貝存儲在各自的cpu的cache中,而且對于其他線程是不可見的。
下圖說明了大致的情況,在左邊cpu執行的線程將共享對象讀取到cache中,并且將他的值改變為2.這個變化對右邊的cpu的其他線程是不可見的,因為對于變量count的更新還沒有被寫回到主內存中。
Paste_Image.png
想要解決這個共享對象可見性的問題,可以使用java的volatile關鍵字(參見筆者的另一篇volatile的博文),這個關鍵字可以保證所給定的變量都是直接從主內存中讀取,而且每當更新時就立即寫回到內存中,所以可以保證變化是及時可見的。
資源競速
如果多個線程共享一個對象,而且多個線程需要更新共享對象中的變量,那么就可能造成資源競速的發生。
假設線程A讀取讀取一個共享對象的變量count到cpu的cache中,同時,線程B也執行同樣的步驟,但是是讀取到一個不同的CPU的cache中,現在線程A給count加一,線程B也做同樣的事情,現在這個變量被加了兩次,分別在不同的cpu的cache中。
如果這兩次遞增操作是被按順序先后執行的,這個變量count就會被加兩次而且比最初的值加了2,寫回到主內存中。
然而,如果這兩個遞增操作是并發執行的,且沒有正確的進行同步操作,寫回內存的時候,更新后的值只會被加一,雖然實際上是進行了兩次遞增操作。
下圖就說明了程序并發執行的時候,產生的資源競速的問題:
Paste_Image.png
想要解決這個問題,我們可以使用java中的synchronize關鍵字。synchronize可以保證只有一個線程能進入那些被聲明為synchronize的代碼段中。同步的線程可以保證所有同步代碼段中的變量都會從內存中讀取,而且當線程離開代碼塊的時候,所有更新后的值都會被寫回主內存中,不管這個變量有沒有被聲明volatile。
小結
本文詳細的剖析了java內存模型和硬件層面的內存模型,并且分析了硬件和java是怎么在內存模型上合作聯系的。這對于我們接下來理解java多線程的概念是及其重要的,打下了牢固的基礎。
總結
以上是生活随笔為你收集整理的java线程内存模型_深度解析Java多线程的内存模型的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: nodejs 定时 mysql_node
- 下一篇: java string 常量池_用了这么