Java的內存模型

在Java語言中,採用的是共享內存模型來實現多線程之間的信息交換和數據同步的。線程之間通過共享程序公共的狀態,通過讀-寫內存中公共狀態的方式來進行隱式的通信。同步指的是程序在控制多個線程之間執行程序的相對順序的機制,在共享內存模型中,同步是顯式的,程序員必須顯式指定某個方法/代碼塊需要在多線程之間互斥執行。下面是小編爲大家帶來的Java的內存模型,歡迎閱讀。

Java的內存模型

  概述

Java內存模型的主要目標是定義程序中各個變量的訪問規則,即在JVM中將變量存儲到內存和從內存中取出變量這樣的底層細節。此處的變量與Java編程裏面的變量有所不同,它包含了實例字段、靜態字段和構成數組對象的元素,但不包含局部變量和方法參數,因爲後者是線程私有的,不會共享,當然不存在數據競爭問題(如果局部變量是一個reference引用類型,它引用的對象在Java堆中可被各個線程共享,但是reference引用本身在Java棧的局部變量表中,是線程私有的)。爲了獲得較高的執行效能,Java內存模型並沒有限制執行引起使用處理器的特定寄存器或者緩存來和主內存進行交互,也沒有限制即時編譯器進行調整代碼執行順序這類優化措施。

JMM規定了所有的變量都存儲在主內存(Main Memory)中。每個線程還有自己的工作內存(Working Memory),線程的工作內存中保存了該線程使用到的變量的主內存的副本拷貝,線程對變量的所有操作(讀取、賦值等)都必須在工作內存中進行,而不能直接讀寫主內存中的變量(volatile變量仍然有工作內存的拷貝,但是由於它特殊的操作順序性規定,所以看起來如同直接在主內存中讀寫訪問一般)。不同的線程之間也無法直接訪問對方工作內存中的變量,線程之間值的傳遞都需要通過主內存來完成。

線程1和線程2要想進行數據的交換一般要經歷下面的步驟:

1.線程1把工作內存1中的更新過的共享變量刷新到主內存中去。

2.線程2到主內存中去讀取線程1刷新過的共享變量,然後copy一份到工作內存2中去。

  內存模型的特性

Java內存模型是圍繞着併發編程中原子性、可見性、有序性這三個特徵來建立的,那我們依次看一下這三個特徵:

  原子性(Atomicity)

原子性是指一個操作不能被打斷,要麼全部執行完畢,要麼不執行。在這點上有點類似於事務操作,要麼全部執行成功,要麼回退到執行該操作之前的狀態。

基本類型數據的訪問大都是原子操作,long 和double類型的變量是64位,但是在32位JVM中,32位的JVM會將64位數據的讀寫操作分爲2次32位的讀寫操作來進行,這就導致了long、double類型的變量在32位虛擬機中是非原子操作,數據有可能會被破壞,也就意味着多個線程在併發訪問的時候是線程非安全的。

下面我們來演示這個32位JVM下,對64位long類型的數據的訪問的問題:

【代碼1】

public class NotAtomicity {

//靜態變量t

public static long t = 0;

//靜態變量t的get方法

public static long getT() {

return t;

}

//靜態變量t的set方法

public static void setT(long t) {

NotAtomicity.t = t;

}

//改變變量t的線程

public static class ChangeT implements Runnable{

private long to;

public ChangeT(long to) {

= to;

}

public void run() {

//不斷的將long變量設值到 t中

while (true) {

(to);

//將當前線程的執行時間片段讓出去,以便由線程調度機制重新決定哪個線程可以執行

d();

}

}

}

//讀取變量t的線程,若讀取的值和設置的值不一致,說明變量t的數據被破壞了,即線程不安全

public static class ReadT implements Runnable{

public void run() {

//不斷的讀取NotAtomicity的t的值

while (true) {

long tmp = ();

//比較是否是自己設值的其中一個

if (tmp != 100L && tmp != 200L && tmp != -300L && tmp != -400L) {

//程序若執行到這裏,說明long類型變量t,其數據已經被破壞了

tln(tmp);

}

////將當前線程的執行時間片段讓出去,以便由線程調度機制重新決定哪個線程可以執行

d();

}

}

}

public static void main(String[] args) {

new Thread(new ChangeT(100L))t();

new Thread(new ChangeT(200L))t();

new Thread(new ChangeT(-300L))t();

new Thread(new ChangeT(-400L))t();

new Thread(new ReadT())t();

}

}

我們創建了4個線程來對long類型的變量t進行賦值,賦值分別爲100,200,-300,-400,有一個線程負責讀取變量t,如果正常的話,讀取到的t的值應該是我們賦值中的一個,但是在32的JVM中(ps: 64位的就別想了),事情會出乎預料。如果程序正常的話,我們控制檯不會有任何的輸出,可實際上,程序一運行,控制檯就輸出了下面的信息:

-4294967096

4294966896

-4294967096

-4294967096

4294966896

之所以會出現上面的情況,是因爲在32位JVM中,64位的long數據的讀和寫都不是原子操作,即不具有原子性,併發的時候相互干擾了。

32位的JVM中,要想保證對long、double類型數據的操作的原子性,可以對訪問該數據的方法進行同步,就像下面的:

【代碼2】

public class Atomicity {

//靜態變量t

public static long t = 0;

//靜態變量t的get方法,同步方法

public synchronized static long getT() {

return t;

}

//靜態變量t的set方法,同步方法

public synchronized static void setT(long t) {

Atomicity.t = t;

}

//改變變量t的線程

public static class ChangeT implements Runnable{

private long to;

public ChangeT(long to) {

= to;

}

public void run() {

//不斷的將long變量設值到 t中

while (true) {

(to);

//將當前線程的執行時間片段讓出去,以便由線程調度機制重新決定哪個線程可以執行

d();

}

}

}

//讀取變量t的線程,若讀取的值和設置的值不一致,說明變量t的數據被破壞了,即線程不安全

public static class ReadT implements Runnable{

public void run() {

//不斷的讀取NotAtomicity的t的值

while (true) {

long tmp = ();

//比較是否是自己設值的其中一個

if (tmp != 100L && tmp != 200L && tmp != -300L && tmp != -400L) {

//程序若執行到這裏,說明long類型變量t,其數據已經被破壞了

tln(tmp);

}

////將當前線程的執行時間片段讓出去,以便由線程調度機制重新決定哪個線程可以執行

d();

}

}

}

public static void main(String[] args) {

new Thread(new ChangeT(100L))t();

new Thread(new ChangeT(200L))t();

new Thread(new ChangeT(-300L))t();

new Thread(new ChangeT(-400L))t();

new Thread(new ReadT())t();

}

}

這樣做的話,可以保證對64位數據操作的原子性。

可見性

一個線程對共享變量做了修改之後,其他的線程立即能夠看到(感知到)該變量這種修改(變化)。

Java內存模型是通過將在工作內存中的變量修改後的值同步到主內存,在讀取變量前從主內存刷新最新值到工作內存中,這種依賴主內存的'方式來實現可見性的。

無論是普通變量還是volatile變量都是如此,區別在於:volatile的特殊規則保證了volatile變量值修改後的新值立刻同步到主內存,每次使用volatile變量前立即從主內存中刷新,因此volatile保證了多線程之間的操作變量的可見性,而普通變量則不能保證這一點。

除了volatile關鍵字能實現可見性之外,還有synchronized,Lock,final也是可以的。

使用synchronized關鍵字,在同步方法/同步塊開始時(Monitor Enter),使用共享變量時會從主內存中刷新變量值到工作內存中(即從主內存中讀取最新值到線程私有的工作內存中),在同步方法/同步塊結束時(Monitor Exit),會將工作內存中的變量值同步到主內存中去(即將線程私有的工作內存中的值寫入到主內存進行同步)。

使用Lock接口的最常用的實現ReentrantLock(重入鎖)來實現可見性:當我們在方法的開始位置執行()方法,這和synchronized開始位置(Monitor Enter)有相同的語義,即使用共享變量時會從主內存中刷新變量值到工作內存中(即從主內存中讀取最新值到線程私有的工作內存中),在方法的最後finally塊裏執行ck()方法,和synchronized結束位置(Monitor Exit)有相同的語義,即會將工作內存中的變量值同步到主內存中去(即將線程私有的工作內存中的值寫入到主內存進行同步)。

final關鍵字的可見性是指:被final修飾的變量,在構造函數數一旦初始化完成,並且在構造函數中並沒有把“this”的引用傳遞出去(“this”引用逃逸是很危險的,其他的線程很可能通過該引用訪問到只“初始化一半”的對象),那麼其他線程就可以看到final變量的值。

  有序性

對於一個線程的代碼而言,我們總是以爲代碼的執行是從前往後的,依次執行的。這麼說不能說完全不對,在單線程程序裏,確實會這樣執行;但是在多線程併發時,程序的執行就有可能出現亂序。用一句話可以總結爲:在本線程內觀察,操作都是有序的;如果在一個線程中觀察另外一個線程,所有的操作都是無序的。前半句是指“線程內表現爲串行語義(WithIn Thread As-if-Serial Semantics)”,後半句是指“指令重排”現象和“工作內存和主內存同步延遲”現象。

Java提供了兩個關鍵字volatile和synchronized來保證多線程之間操作的有序性,volatile關鍵字本身通過加入內存屏障來禁止指令的重排序,而synchronized關鍵字通過一個變量在同一時間只允許有一個線程對其進行加鎖的規則來實現,在單線程程序中,不會發生“指令重排”和“工作內存和主內存同步延遲”現象,只在多線程程序中出現。

  happens-before原則

Java內存模型中定義的兩項操作之間的次序關係,如果說操作A先行發生於操作B,操作A產生的影響能作B觀察到,“影響”包含了修改了內存享變量的值、發送了消息、調用了方法等。

下面是Java內存模型下一些”天然的“happens-before關係,這些happens-before關係無須任何同步器協助就已經存在,可以在編碼中直接使用。如果兩個操作之間的關係不在此列,並且無法從下列規則推導出來的話,它們就沒有順序性保障,虛擬機可以對它們進行隨意地重排序。

程序次序規則(Pragram Order Rule):在一個線程內,按照程序代碼順序,書寫在前面的操作先行發生於書寫在後面的操作。準確地說應該是控制流順序而不是程序代碼順序,因爲要考慮分支、循環結構。

管程鎖定規則(Monitor Lock Rule):一個unlock操作先行發生於後面對同一個鎖的lock操作。這裏必須強調的是同一個鎖,而”後面“是指時間上的先後順序。

volatile變量規則(Volatile Variable Rule):對一個volatile變量的寫操作先行發生於後面對這個變量的讀取操作,這裏的”後面“同樣指時間上的先後順序。

線程啓動規則(Thread Start Rule):Thread對象的start()方法先行發生於此線程的每一個動作。

線程終於規則(Thread Termination Rule):線程中的所有操作都先行發生於對此線程的終止檢測,我們可以通過()方法結束,ive()的返回值等作段檢測到線程已經終止執行。

線程中斷規則(Thread Interruption Rule):對線程interrupt()方法的調用先行發生於被中斷線程的代碼檢測到中斷事件的發生,可以通過rrupted()方法檢測是否有中斷髮生。

對象終結規則(Finalizer Rule):一個對象初始化完成(構造方法執行完成)先行發生於它的finalize()方法的開始。

傳遞性(Transitivity):如果操作A先行發生於操作B,操作B先行發生於操作C,那就可以得出操作A先行發生於操作C的結論。

一個操作”時間上的先發生“不代表這個操作會是“先行發生",那如果一個操作"先行發生"是否就能推導出這個操作必定是"時間上的先發生"呢?也是不成立的,一個典型的例子就是指令重排序。所以時間上的先後順序與happens-before原則之間基本沒有什麼關係,所以衡量併發安全問題一切必須以happens-before 原則爲準。