JVM能夠看做是一個完整的計算機系統,天然會有本身的內存模型,就像物理機有RAM同樣.Java內存模型決定了Java是如何與物理機內存打交道的.若是你想編寫出具備肯定行爲的多線程併發程序,就須要對Java內存模型有必定的瞭解.jdk1.5以前的內存模型存在必定的缺陷,所以在jdk1.5以後從新發布了內存模型,這個內存模型一直沿用到jdk1.8.html
在Java內存模型中,將內存區域劃分爲線程棧和堆.以下圖展現了Java內存模型的邏輯平面圖:java
在java中,每建立一個線程,JVM就會在內存中建立一個線程棧,該線程棧中的信息僅會被當前線程訪問,對其餘線程是不可見的.且每當線程運行時,線程棧中的信息都會獲得更新.緩存
線程棧用於存儲線程執行過程當中所有方法的所有局部變量以及線程當前執行方法的現場信息,方便用於在線程切換後恢復運行.安全
線程棧中存放的數據類型有基礎數據類型(short, int, long, float, double, boolean, byte, char等)和引用類型,而引用類型引用的具體對象則存放在堆內存中.多線程
儘管多個線程執行的是同一段代碼,他們各自在線程棧中都有一份局部變量的拷貝,互不影響,各自獨立.架構
儘管線程能夠將本身的局部變量傳遞給另外一個線程,然而其餘線程僅能獲得這個線程局部變量的拷貝,並不能訪問到這個線程局部變量自己.併發
在Java應用中建立的全部對象都會存儲在堆中,不管是哪一個線程建立的對象.這些對象包含基礎數據類型的引用類型(Integer, Long等).post
對象的成員變量會跟隨對象存儲在堆中,而在方法內建立和使用的對象也會存儲在堆中,而存儲在線程棧中的僅僅是該對象的引用.一個對象做爲另外一個對象的成員變量也同樣會存儲在堆中.this
下圖以邏輯平面圖的方式展現上文說起的情形:編碼
圖中展現了兩個線程在java內存模型中的邏輯平面圖.兩個線程棧中各自有兩個方法,方法A和方法B,方法A中有兩個局部變量,方法B中有一個局部變量.其中局部變量1爲基礎類型,局部變量2爲引用類型,兩個線程棧的局部變量2都指向了堆中的對象3,其中對象3中有兩個成員變量,成員變量都爲引用類型,分別指向對象2和對象4.兩個線程棧中的局部變量3都爲引用類型,分別指向堆中的對象1和對象5.
經過編碼實例來覆蓋上文說起的情形,咱們建立一個對象JMMExample類,用於模擬上圖中存儲實例的情形.
public class JMMExample {
public static class Object1OrObject5 {
private String str = "obj1 or obj5";
}
public static class Object2 {
private String str = "obj2";
}
public static class Object3 {
public static Object3 getInstance() {
return new Object3();
}
private Object2 obj2;
private Object4 obj4;
public Object3() {
this.obj2 = new Object2();
this.obj4 = new Object4();
}
public Object2 getObj2() {
return obj2;
}
public Object4 getObj4() {
return obj4;
}
}
public static class Object4 {
private String str = "obj4";
}
public void methodA(Object3 localVariable2) {
Thread thread = Thread.currentThread();
System.out.println(thread.getName() + " using localVariable2( " + localVariable2 + " )");
int localVariable1 = 10;
System.out.println(thread.getName() + " using localVariable1( " + localVariable1 + " )");
Object2 obj2 = localVariable2.getObj2();
Object4 obj4 = localVariable2.getObj4();
System.out.println(thread.getName() + " using localVariable2 point to ( " + obj2 + " )");
System.out.println(thread.getName() + " using localVariable2 point to ( " + obj4 + " )");
methodB();
}
public void methodB() {
Thread thread = Thread.currentThread();
Object1OrObject5 obj = new Object1OrObject5();
System.out.println(thread.getName() + " using localVariable3 point to ( " + obj + " )");
}
public static void main(String[] args) {
final Object3 obj3 = Object3.getInstance();
final JMMExample jmmExample = new JMMExample();
Runnable myRunnable = () -> jmmExample.methodA(obj3);
IntStream.range(1, 3)
.forEach(i -> new Thread(myRunnable, "Thread-" + i).start());
}
}
複製代碼
執行結果:
Thread-1 using localVariable2( org.menfre.JMMExample$Object3@41d5399c
)
Thread-1 using localVariable1( 10 )
Thread-2 using localVariable2( org.menfre.JMMExample$Object3@41d5399c
)
Thread-2 using localVariable1( 10 )
Thread-2 using localVariable2 point to ( org.menfre.JMMExample$Object2@4f12cfd6
)
Thread-1 using localVariable2 point to ( org.menfre.JMMExample$Object2@4f12cfd6
)
Thread-1 using localVariable2 point to ( org.menfre.JMMExample$Object4@7c716752
)
Thread-2 using localVariable2 point to ( org.menfre.JMMExample$Object4@7c716752
)
Thread-1 using localVariable3 point to ( org.menfre.JMMExample$Object1OrObject5@4b2d52ec
)
Thread-2 using localVariable3 point to ( org.menfre.JMMExample$Object1OrObject5@72b696ac
)
從結果咱們能夠看出線程1和線程2各自將局部變量1和2加載到線程棧中,其中局部變量1爲int類型,數值爲10,局部變量2爲引用類型,指向對象Object3,從運行結果看出他們指向的對象爲同一個.隨後線程1和線程2經過Object3的成員變量訪問到Object2和Object4,從運行結果能夠看出Object2和Object4也爲同一個對象.再而後線程1和2分別建立了各自的局部變量3,從運行結果能夠看出兩個線程的局部變量3指向的對象是不一樣的,這符合上文說起的Object1和Object5.
物理機內存架構跟java內存模型不太同樣,但瞭解物理機內存架構有助於理解java內存模型與物理機內存之間的交互。
物理機內存架構邏輯平面圖以下:
現代計算機一般擁有兩個或兩個以上的cpu數量,有些cpu甚至擁有多個核心。這使得多個線程同時執行成爲可能。在同一時間點,每一個線程能夠交由一個cpu進行調度,多個線程能夠同時執行。若你的應用支持多線程,那麼你的程序將會在多個cpu中執行。
一般物理機內存會有三層架構,分別是主存,cpu緩存(cpu緩存可能會有多級,如1~3不等,但不影響理解),還有位於cpu中的多組寄存器。主存的容量通常比cpu緩存和寄存器的容量大得多,而cpu緩存容量會比寄存器大。
一般寄存器的讀寫速度會大於cpu緩存,而cpu緩存的讀寫速度會大於主存。cpu執行時會將主存的部分數據加載到cpu緩存,一樣會將cpu緩存中的部分數據加載到寄存器中,而後再對寄存器中的數值進行操做。
cpu執行結束後會將結果回寫到cpu緩存,但cpu緩存中的數據更新後不會當即寫回到主存中,而是當cpu須要將其餘數據加載到cpu緩存中時,纔將cpu緩存中更新後的數據回寫到主存。
cpu緩存不會一次性的寫入和寫出整個緩存區,而是分塊的進行寫入和寫出。cpu緩存中分塊單位稱爲「cache lines」。
物理機內存並不會區分Java內存模型中的線程棧和堆。會將線程棧中的局部變量和堆中的對象無差異的載入主存中。且不管是線程棧中的局部變量仍是堆中的對象都會出如今物理機內存三層架構中。以下邏輯平面圖所示:
一旦變量和對象被載入到多個存儲區域後,就會暴露出特定的問題,最主要的問題是以下兩種;
若一個線程將主存中的共享變量加載到cpu緩存中操做,隨後另外一個線程將主存中的共享變量加載到cpu緩存中,此時第一個線程在cpu緩存中對共享變量進行的更新對另外一個線程不可見。以下邏輯平面圖所示:
圖中主存中存儲有共享變量count,count值等於1。在沒有volatile
修飾符修飾的狀況下,此時兩個線程分別將共享變量加載到cpu緩存中,第一個線程對count進行+1操做,此時count數值更新爲2,但cpu緩存並不會將更新後的數值當即寫回主存,此時線程2加載到cpu緩存中的count並非線程1更新後的數值。
java中,能夠用volatile
修飾共享變量,來讓共享變量獲得更新後當即寫回主存,以解決上述問題。
若兩個線程同時將主存中的共享變量加載到cpu緩存中進行操做,同時更新共享變量,寫回主存後的預期變化是兩次更新都能對主存中的共享變量生效。但事實是,在沒有任何同步措施的狀況下,兩個更新被回寫到主存後,僅會保留最後一次的更新。以下邏輯平面圖所示:
圖中主存中存儲有共享變量count,count值等於1。在沒有任何同步措施的狀況下,此時兩個線程同時將共享變量加載到cpu緩存中,兩個線程都對count進行+1操做,此時共享變量在cpu緩存中有兩個版本。在兩個線程將更新後的count寫回主存時,主存中的共享變量應該被更新成3,但此時僅會有一個+1操做生效,即count會被更新爲2.
java中,能夠用synchronized
來修飾臨界區代碼,生成同步塊。同步塊中的變量一次僅能被一個線程訪問加載到cpu緩存中,且cpu緩存中對同步塊中的共享變量的更新會被當即寫回主存,不管該共享變量有無volatile
修飾。
該系列博文爲筆者複習基礎所著譯文或理解後的產物,複習原文來自Jakob Jenkov所著Java Concurrency and Multithreading Tutorial