當前位置:才華齋>計算機>java語言>

Java的記憶體模型

java語言 閱讀(2.89W)

在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 原則為準。