❶ java 里 volatile 關鍵字有什麼特性 是否能保證線程安全
任何被volatile修飾的變數,都不拷貝副本到工作內存,任何修改都及時寫在主存。因此對於Valatile修飾的變數的修改,所有線程馬上就能看到,但是volatile不能保證對變數的修改是有序的。什麼意思呢?假如有這樣的代碼:
Java代碼
public class VolatileTest
{
public volatile int a;
public void add(int count)
{
a=a+count;
}
}
當一個VolatileTest對象被多個線程共享,a的值不一定是正確的,因為a=a+count包含了好幾步操作,而此時多個線程的執行是無序的,因為沒有任何機制來保證多個線程的執行有序性和原子性。volatile存在的意義是,任何線程對a的修改,都會馬上被其他線程讀取到,因為直接操作主存,沒有線程對工作內存和主存的同步。所以,volatile的使用場景是有限的,在有限的一些情形下可以使用 volatile 變數替代鎖。要使 volatile 變數提供理想的線程安全,必須同時滿足下面兩個條件:1)對變數的寫操作不依賴於當前值。2)該變數沒有包含在具有其他變數的不變式中
volatile只保證了可見性,所以Volatile適合直接賦值的場景,如
Java代碼
public class VolatileTest
{
public volatile int a; public void setA(int a)
{
this.a=a;
}
}
public class VolatileTest
{
public volatile int a;www.gzlij.com public void setA(int a)
{
this.a=a;
}
}
4
在沒有volatile聲明時,多線程環境下,a的最終值不一定是正確的,因為this.a=a;涉及到給a賦值和將a同步回主存的步驟,這個順序可能被打亂。如果用volatile聲明了,讀取主存副本到工作內存和同步a到主存的步驟,相當於是一個原子操作。所以簡單來說,volatile適合這種場景:一個變數被多個線程共享,線程直接給這個變數賦值。這是一種很簡單的同步場景,這時候使用volatile的開銷將會非常小。
❷ java之用volatile和不用volatile的區別
在當前的Java內存模型下,線程可以把變數保存在本地內存(比如機器的寄存器)中,而不是直接在主存中進行讀寫。這就可能造成一個線程在主存中修改了一個變數的值,而另外一個線程還繼續使用它在寄存器中的變數值的拷貝,造成數據的不一致。
要解決這個問題,只需要像在本程序中的這樣,把該變數聲明為volatile(不穩定的)即可,這就指示JVM,這個變數是不穩定的,每次使用它都到主存中進行讀取。一般說來,多任務環境下各任務間共享的標志都應該加volatile修飾。
Volatile修飾的成員變數在每次被線程訪問時,都強迫從共享內存中重讀該成員變數的值。而且,當成員變數發生變化時,強迫線程將變化值回寫到共享內存。這樣在任何時刻,兩個不同的線程總是看到某個成員變數的同一個值。
用volatile和不用volatile的區別,運行一下,就知道了。
不用volatile:
packagecom.keyword;
{
privatestaticbooleanbChanged;
publicstaticvoidmain(String[]args)throwsInterruptedException{
newThread(){
@Override
publicvoidrun(){
for(;;){
if(bChanged==!bChanged){
System.out.println("!=");
System.exit(0);
}
}
}
}.start();
Thread.sleep(1);
newThread(){
@Override
publicvoidrun(){
for(;;){
bChanged=!bChanged;
}
}
}.start();
}
}
運行後,程序進入死循環了,一直在運行。
用volatile:
packagecom.keyword;
publicclassTestWithVolatile{
;
publicstaticvoidmain(String[]args)throwsInterruptedException{
newThread(){
@Override
publicvoidrun(){
for(;;){
if(bChanged==!bChanged){
System.out.println("!=");
System.exit(0);
}
}
}
}.start();
Thread.sleep(1);
newThread(){
@Override
publicvoidrun(){
for(;;){
bChanged=!bChanged;
}
}
}.start();
}
}
程序輸出!=,然後馬上退出。
但是,很多情況下,用不用volatile,感覺不出什麼區別,什麼時候要用volatile呢?看看JDK里使用volatile的類。
比如java.util.regex.Pattern里的變數:
=false;
還有,java.lang.System的變數:
=null;
一般就是初始化的時候,需要用到volatile。
java.util.Scanner里的變數,如:
;
;
;
初始化boolPattern的代碼:
(){
Patternbp=boolPattern;
if(bp==null)
boolPattern=bp=Pattern.compile(BOOLEAN_PATTERN,
Pattern.CASE_INSENSITIVE);
returnbp;
}
上面的情況,可以使用synchronized來對boolPattern加鎖,但是synchronized開銷比volatile大,volatile能夠勝任上面的工作。
volatile不保證原子操作,所以,很容易讀到臟數據。
使用建議:
在兩個或者更多的線程訪問的成員變數上使用volatile。
當要訪問的變數已在synchronized代碼塊中,或者為常量時,不必使用。
❸ java內存模型的JMM簡介
1)JSR133:
在Java語言規范裡面指出了JMM是一個比較開拓性的嘗試,這種嘗試視圖定義一個一致的、跨平台的內存模型,但是它有一些比較細微而且很重要的缺點。其實Java語言裡面比較容易混淆的關鍵字主要是synchronized和volatile,也因為這樣在開發過程中往往開發者會忽略掉這些規則,這也使得編寫同步代碼比較困難。
JSR133本身的目的是為了修復原本JMM的一些缺陷而提出的,其本身的制定目標有以下幾個: 保留目JVM的安全保證,以進行類型的安全檢查: 提供(out-of-thin-air safety)無中生有安全性,這樣「正確同步的」應該被正式而且直觀地定義 程序員要有信心開發多線程程序,當然沒有其他辦法使得並發程序變得很容易開發,但是該規范的發布主要目標是為了減輕程序員理解內存模型中的一些細節負擔 提供大范圍的流行硬體體系結構上的高性能JVM實現,現在的處理器在它們的內存模型上有著很大的不同,JMM應該能夠適合於實際的盡可能多的體系結構而不以性能為代價,這也是Java跨平台型設計的基礎 提供一個同步的習慣用法,以允許發布一個對象使他不用同步就可見,這種情況又稱為初始化安全(initialization safety)的新的安全保證 對現有代碼應該只有最小限度的影響 2)同步、非同步【這里僅僅指概念上的理解,不牽涉到計算機底層基礎的一些操作】:
在系統開發過程,經常會遇到這幾個基本概念,不論是網路通訊、對象之間的消息通訊還是Web開發人員常用的Http請求都會遇到這樣幾個概念,經常有人提到Ajax是非同步通訊方式,那麼究竟怎樣的方式是這樣的概念描述呢?
同步:同步就是在發出一個功能調用的時候,在沒有得到響應之前,該調用就不返回,按照這樣的定義,其實大部分程序的執行都是同步調用的,一般情況下,在描述同步和非同步操作的時候,主要是指代需要其他部件協作處理或者需要協作響應的一些任務處理。比如有一個線程A,在A執行的過程中,可能需要B提供一些相關的執行數據,當然觸發B響應的就是A向B發送一個請求或者說對B進行一個調用操作,如果A在執行該操作的時候是同步的方式,那麼A就會停留在這個位置等待B給一個響應消息,在B沒有任何響應消息回來的時候,A不能做其他事情,只能等待,那麼這樣的情況,A的操作就是一個同步的簡單說明。
非同步:非同步就是在發出一個功能調用的時候,不需要等待響應,繼續進行它該做的事情,一旦得到響應了過後給予一定的處理,但是不影響正常的處理過程的一種方式。比如有一個線程A,在A執行的過程中,同樣需要B提供一些相關數據或者操作,當A向B發送一個請求或者對B進行調用操作過後,A不需要繼續等待,而是執行A自己應該做的事情,一旦B有了響應過後會通知A,A接受到該非同步請求的響應的時候會進行相關的處理,這種情況下A的操作就是一個簡單的非同步操作。
3)可見性、可排序性
Java內存模型的兩個關鍵概念:可見性(Visibility)和可排序性(Ordering)
開發過多線程程序的程序員都明白,synchronized關鍵字強制實施一個線程之間的互斥鎖(相互排斥),該互斥鎖防止每次有多個線程進入一個給定監控器所保護的同步語句塊,也就是說在該情況下,執行程序代碼所獨有的某些內存是獨占模式,其他的線程是不能針對它執行過程所獨占的內存進行訪問的,這種情況稱為該內存不可見。但是在該模型的同步模式中,還有另外一個方面:JMM中指出了,JVM在處理該強制實施的時候可以提供一些內存的可見規則,在該規則裡面,它確保當存在一個同步塊時,緩存被更新,當輸入一個同步塊時,緩存失效。因此在JVM內部提供給定監控器保護的同步塊之中,一個線程所寫入的值對於其餘所有的執行由同一個監控器保護的同步塊線程來說是可見的,這就是一個簡單的可見性的描述。這種機器保證編譯器不會把指令從一個同步塊的內部移到外部,雖然有時候它會把指令由外部移動到內部。JMM在預設情況下不做這樣的保證——只要有多個線程訪問相同變數時必須使用同步。簡單總結:
可見性就是在多核或者多線程運行過程中內存的一種共享模式,在JMM模型裡面,通過並發線程修改變數值的時候,必須將線程變數同步回主存過後,其他線程才可能訪問到。
可排序性提供了內存內部的訪問順序,在不同的程序針對不同的內存塊進行訪問的時候,其訪問不是無序的,比如有一個內存塊,A和B需要訪問的時候,JMM會提供一定的內存分配策略有序地分配它們使用的內存,而在內存的調用過程也會變得有序地進行,內存的折中性質可以簡單理解為有序性。而在Java多線程程序裡面,JMM通過Java關鍵字volatile來保證內存的有序訪問。 1)簡單分析:
Java語言規范中提到過,JVM中存在一個主存區(Main Memory或Java Heap Memory),Java中所有變數都是存在主存中的,對於所有線程進行共享,而每個線程又存在自己的工作內存(Working Memory),工作內存中保存的是主存中某些變數的拷貝,線程對所有變數的操作並非發生在主存區,而是發生在工作內存中,而線程之間是不能直接相互訪問,變數在程序中的傳遞,是依賴主存來完成的。而在多核處理器下,大部分數據存儲在高速緩存中,如果高速緩存不經過內存的時候,也是不可見的一種表現。在Java程序中,內存本身是比較昂貴的資源,其實不僅僅針對Java應用程序,對操作系統本身而言內存也屬於昂貴資源,Java程序在性能開銷過程中有幾個比較典型的可控制的來源。synchronized和volatile關鍵字提供的內存中模型的可見性保證程序使用一個特殊的、存儲關卡(memory barrier)的指令,來刷新緩存,使緩存無效,刷新硬體的寫緩存並且延遲執行的傳遞過程,無疑該機制會對Java程序的性能產生一定的影響。
JMM的最初目的,就是為了能夠支持多線程程序設計的,每個線程可以認為是和其他線程不同的CPU上運行,或者對於多處理器的機器而言,該模型需要實現的就是使得每一個線程就像運行在不同的機器、不同的CPU或者本身就不同的線程上一樣,這種情況實際上在項目開發中是常見的。對於CPU本身而言,不能直接訪問其他CPU的寄存器,模型必須通過某種定義規則來使得線程和線程在工作內存中進行相互調用而實現CPU本身對其他CPU、或者說線程對其他線程的內存中資源的訪問,而表現這種規則的運行環境一般為運行該程序的運行宿主環境(操作系統、伺服器、分布式系統等),而程序本身表現就依賴於編寫該程序的語言特性,這里也就是說用Java編寫的應用程序在內存管理中的實現就是遵循其部分原則,也就是前邊提及到的JMM定義了Java語言針對內存的一些的相關規則。然而,雖然設計之初是為了能夠更好支持多線程,但是該模型的應用和實現當然不局限於多處理器,而在JVM編譯器編譯Java編寫的程序的時候以及運行期執行該程序的時候,對於單CPU的系統而言,這種規則也是有效的,這就是是上邊提到的線程和線程之間的內存策略。JMM本身在描述過程沒有提過具體的內存地址以及在實現該策略中的實現方法是由JVM的哪一個環節(編譯器、處理器、緩存控制器、其他)提供的機制來實現的,甚至針對一個開發非常熟悉的程序員,也不一定能夠了解它內部對於類、對象、方法以及相關內容的一些具體可見的物理結構。相反,JMM定義了一個線程與主存之間的抽象關系,其實從上邊的圖可以知道,每一個線程可以抽象成為一個工作內存(抽象的高速緩存和寄存器),其中存儲了Java的一些值,該模型保證了Java裡面的屬性、方法、欄位存在一定的數學特性,按照該特性,該模型存儲了對應的一些內容,並且針對這些內容進行了一定的序列化以及存儲排序操作,這樣使得Java對象在工作內存裡面被JVM順利調用,(當然這是比較抽象的一種解釋)既然如此,大多數JMM的規則在實現的時候,必須使得主存和工作內存之間的通信能夠得以保證,而且不能違反內存模型本身的結構,這是語言在設計之處必須考慮到的針對內存的一種設計方法。這里需要知道的一點是,這一切的操作在Java語言裡面都是依靠Java語言自身來操作的,因為Java針對開發人員而言,內存的管理在不需要手動操作的情況下本身存在內存的管理策略,這也是Java自己進行內存管理的一種優勢。
[1]原子性(Atomicity):
這一點說明了該模型定義的規則針對原子級別的內容存在獨立的影響,對於模型設計最初,這些規則需要說明的僅僅是最簡單的讀取和存儲單元寫入的的一些操作,這種原子級別的包括——實例、靜態變數、數組元素,只是在該規則中不包括方法中的局部變數。
[2]可見性(Visibility):
在該規則的約束下,定義了一個線程在哪種情況下可以訪問另外一個線程或者影響另外一個線程,從JVM的操作上講包括了從另外一個線程的可見區域讀取相關數據以及將數據寫入到另外一個線程內。
[3]可排序性(Ordering):
該規則將會約束任何一個違背了規則調用的線程在操作過程中的一些順序,排序問題主要圍繞了讀取、寫入和賦值語句有關的序列。
如果在該模型內部使用了一致的同步性的時候,這些屬性中的每一個屬性都遵循比較簡單的原則:和所有同步的內存塊一樣,每個同步塊之內的任何變化都具備了原子性以及可見性,和其他同步方法以及同步塊遵循同樣一致的原則,而且在這樣的一個模型內,每個同步塊不能使用同一個鎖,在整個程序的調用過程是按照編寫的程序指定指令運行的。即使某一個同步塊內的處理可能會失效,但是該問題不會影響到其他線程的同步問題,也不會引起連環失效。簡單講:當程序運行的時候使用了一致的同步性的時候,每個同步塊有一個獨立的空間以及獨立的同步控制器和鎖機制,然後對外按照JVM的執行指令進行數據的讀寫操作。這種情況使得使用內存的過程變得非常嚴謹!
如果不使用同步或者說使用同步不一致(這里可以理解為非同步,但不一定是非同步操作),該程序執行的答案就會變得極其復雜。而且在這樣的情況下,該內存模型處理的結果比起大多數程序員所期望的結果而言就變得十分脆弱,甚至比起JVM提供的實現都脆弱很多。因為這樣所以出現了Java針對該內存操作的最簡單的語言規范來進行一定的習慣限制,排除該情況發生的做法在於:
JVM線程必須依靠自身來維持對象的可見性以及對象自身應該提供相對應的操作而實現整個內存操作的三個特性,而不是僅僅依靠特定的修改對象狀態的線程來完成如此復雜的一個流程。
[4]三個特性的解析(針對JMM內部):
原子性(Atomicity):
訪問存儲單元內的任何類型的欄位的值以及對其更新操作的時候,除開long類型和double類型,其他類型的欄位是必須要保證其原子性的,這些欄位也包括為對象服務的引用。此外,該原子性規則擴展可以延伸到基於long和double的另外兩種類型:volatile long和volatile double(volatile為java關鍵字),沒有被volatile聲明的long類型以及double類型的欄位值雖然不保證其JMM中的原子性,但是是被允許的。針對non-long/non-double的欄位在表達式中使用的時候,JMM的原子性有這樣一種規則:如果你獲得或者初始化該值或某一些值的時候,這些值是由其他線程寫入,而且不是從兩個或者多個線程產生的數據在同一時間戳混合寫入的時候,該欄位的原子性在JVM內部是必須得到保證的。也就是說JMM在定義JVM原子性的時候,只要在該規則不違反的條件下,JVM本身不去理睬該數據的值是來自於什麼線程,因為這樣使得Java語言在並行運算的設計的過程中針對多線程的原子性設計變得極其簡單,而且即使開發人員沒有考慮到最終的程序也沒有太大的影響。再次解釋一下:這里的原子性指的是原子級別的操作,比如最小的一塊內存的讀寫操作,可以理解為Java語言最終編譯過後最接近內存的最底層的操作單元,這種讀寫操作的數據單元不是變數的值,而是本機碼,也就是前邊在講《Java基礎知識》中提到的由運行器解釋的時候生成的Native Code。
可見性(Visibility):
當一個線程需要修改另外線程的可見單元的時候必須遵循以下原則: 一個寫入線程釋放的同步鎖和緊隨其後進行讀取的讀線程的同步鎖是同一個從本質上講,釋放鎖操作強迫它的隸屬線程【釋放鎖的線程】從工作內存中的寫入緩存裡面刷新(專業上講這里不應該是刷新,可以理解為提供)數據(flush操作),然後獲取鎖操作使得另外一個線程【獲得鎖的線程】直接讀取前一個線程可訪問域(也就是可見區域)的欄位的值。因為該鎖內部提供了一個同步方法或者同步塊,該同步內容具有線程排他性,這樣就使得上邊兩個操作只能針對單一線程在同步內容內部進行操作,這樣就使得所有操作該內容的單一線程具有該同步內容(加鎖的同步方法或者同步塊)內的線程排他性,這種情況的交替也可以理解為具有「短暫記憶效應」。這里需要理解的是同步的雙重含義:使用鎖機制允許基於高層同步協議進行處理操作,這是最基本的同步;同時系統內存(很多時候這里是指基於機器指令的底層存儲關卡memory barrier,前邊提到過)在處理同步的時候能夠跨線程操作,使得線程和線程之間的數據是同步的。這樣的機制也折射出一點,並行編程相對於順序編程而言,更加類似於分布式編程。後一種同步可以作為JMM機制中的方法在一個線程中運行的效果展示,注意這里不是多個線程運行的效果展示,因為它反應了該線程願意發送或者接受的雙重操作,並且使得它自己的可見區域可以提供給其他線程運行或者更新,從這個角度來看,使用鎖和消息傳遞可以視為相互之間的變數同步,因為相對其他線程而言,它的操作針對其他線程也是對等的。 一旦某個欄位被申明為volatile,在任何一個寫入線程在工作內存中刷新緩存的之前需要進行進一步的內存操作,也就是說針對這樣的欄位進行立即刷新,可以理解為這種volatile不會出現一般變數的緩存操作,而讀取線程每次必須根據前一個線程的可見域裡面重新讀取該變數的值,而不是直接讀取。 當某個線程第一次去訪問某個對象的域的時候,它要麼初始化該對象的值,要麼從其他寫入線程可見域裡面去讀取該對象的值;這里結合上邊理解,在滿足某種條件下,該線程對某對象域的值的讀取是直接讀取,有些時候卻需要重新讀取。這里需要小心一點的是,在並發編程裡面,不好的一個實踐就是使用一個合法引用去引用不完全構造的對象,這種情況在從其他寫入線程可見域裡面進行數據讀取的時候發生頻率比較高。從編程角度上講,在構造函數裡面開啟一個新的線程是有一定的風險的,特別是該類是屬於一個可子類化的類的時候。Thread.start由調用線程啟動,然後由獲得該啟動的線程釋放鎖具有相同的「短暫記憶效應」,如果一個實現了Runnable介面的超類在子類構造子執行之前調用了Thread(this).start()方法,那麼就可能使得該對象在線程方法run執行之前並沒有被完全初始化,這樣就使得一個指向該對象的合法引用去引用了不完全構造的一個對象。同樣的,如果創建一個新的線程T並且啟動該線程,然後再使用線程T來創建對象X,這種情況就不能保證X對象裡面所有的屬性針對線程T都是可見的除非是在所有針對X對象的引用中進行同步處理,或者最好的方法是在T線程啟動之前創建對象X。 若一個線程終止,所有的變數值都必須從工作內存中刷到主存,比如,如果一個同步線程因為另一個使用Thread.join方法的線程而終止,那麼該線程的可見域針對那個線程而言其發生的改變以及產生的一些影響是需要保證可知道的。 注意:如果在同一個線程裡面通過方法調用去傳一個對象的引用是絕對不會出現上邊提及到的可見性問題的。JMM保證所有上邊的規定以及關於內存可見性特性的描述——一個特殊的更新、一個特定欄位的修改都是某個線程針對其他線程的一個「可見性」的概念,最終它發生的場所在內存模型中Java線程和線程之間,至於這個發生時間可以是一個任意長的時間,但是最終會發生,也就是說,Java內存模型中的可見性的特性主要是針對線程和線程之間使用內存的一種規則和約定,該約定由JMM定義。
不僅僅如此,該模型還允許不同步的情況下可見性特性。比如針對一個線程提供一個對象或者欄位訪問域的原始值進行操作,而針對另外一個線程提供一個對象或者欄位刷新過後的值進行操作。同樣也有可能針對一個線程讀取一個原始的值以及引用對象的對象內容,針對另外一個線程讀取一個刷新過後的值或者刷新過後的引用。
盡管如此,上邊的可見性特性分析的一些特徵在跨線程操作的時候是有可能失敗的,而且不能夠避免這些故障發生。這是一個不爭的事實,使用同步多線程的代碼並不能絕對保證線程安全的行為,只是允許某種規則對其操作進行一定的限制,但是在最新的JVM實現以及最新的Java平台中,即使是多個處理器,通過一些工具進行可見性的測試發現其實是很少發生故障的。跨線程共享CPU的共享緩存的使用,其缺陷就在於影響了編譯器的優化操作,這也體現了強有力的緩存一致性使得硬體的價值有所提升,因為它們之間的關系在線程與線程之間的復雜度變得更高。這種方式使得可見度的自由測試顯得更加不切實際,因為這些錯誤的發生極為罕見,或者說在平台上我們開發過程中根本碰不到。在並行程開發中,不使用同步導致失敗的原因也不僅僅是對可見度的不良把握導致的,導致其程序失敗的原因是多方面的,包括緩存一致性、內存一致性問題等。
可排序性(Ordering):
可排序規則在線程與線程之間主要有下邊兩點: 從操作線程的角度看來,如果所有的指令執行都是按照普通順序進行,那麼對於一個順序運行的程序而言,可排序性也是順序的 從其他操作線程的角度看來,排序性如同在這個線程中運行在非同步方法中的一個「間諜」,所以任何事情都有可能發生。唯一有用的限制是同步方法和同步塊的相對排序,就像操作volatile欄位一樣,總是保留下來使用 【*:如何理解這里「間諜」的意思,可以這樣理解,排序規則在本線程裡面遵循了第一條法則,但是對其他線程而言,某個線程自身的排序特性可能使得它不定地訪問執行線程的可見域,而使得該線程對本身在執行的線程產生一定的影響。舉個例子,A線程需要做三件事情分別是A1、A2、A3,而B是另外一個線程具有操作B1、B2,如果把參考定位到B線程,那麼對A線程而言,B的操作B1、B2有可能隨時會訪問到A的可見區域,比如A有一個可見區域a,A1就是把a修改稱為1,但是B線程在A線程調用了A1過後,卻訪問了a並且使用B1或者B2操作使得a發生了改變,變成了2,那麼當A按照排序性進行A2操作讀取到a的值的時候,讀取到的是2而不是1,這樣就使得程序最初設計的時候A線程的初衷發生了改變,就是排序被打亂了,那麼B線程對A線程而言,其身份就是「間諜」,而且需要注意到一點,B線程的這些操作不會和A之間存在等待關系,那麼B線程的這些操作就是非同步操作,所以針對執行線程A而言,B的身份就是「非同步方法中的『間諜』。】
同樣的,這僅僅是一個最低限度的保障性質,在任何給定的程序或者平台,開發中有可能發現更加嚴格的排序,但是開發人員在設計程序的時候不能依賴這種排序,如果依賴它們會發現測試難度會成指數級遞增,而且在復合規定的時候會因為不同的特性使得JVM的實現因為不符合設計初衷而失敗。
注意:第一點在JLS(Java Language Specification)的所有討論中也是被採用的,例如算數表達式一般情況都是從上到下、從左到右的順序,但是這一點需要理解的是,從其他操作線程的角度看來這一點又具有不確定性,對線程內部而言,其內存模型本身是存在排序性的。【*:這里討論的排序是最底層的內存裡面執行的時候的NativeCode的排序,不是說按照順序執行的Java代碼具有的有序性質,本文主要分析的是JVM的內存模型,所以希望讀者明白這里指代的討論單元是內存區。】 JMM最初設計的時候存在一定的缺陷,這種缺陷雖然現有的JVM平台已經修復,但是這里不得不提及,也是為了讀者更加了解JMM的設計思路,這一個小節的概念可能會牽涉到很多更加深入的知識,如果讀者不能讀懂沒有關系先看了文章後邊的章節再返回來看也可以。
1)問題1:不可變對象不是不可變的
學過Java的朋友都應該知道Java中的不可變對象,這一點在本文最後講解String類的時候也會提及,而JMM最初設計的時候,這個問題一直都存在,就是:不可變對象似乎可以改變它們的值(這種對象的不可變指通過使用final關鍵字來得到保證),(Publis Service Reminder:讓一個對象的所有欄位都為final並不一定使得這個對象不可變——所有類型還必須是原始類型而不能是對象的引用。而不可變對象被認為不要求同步的。但是,因為在將內存寫方面的更改從一個線程傳播到另外一個線程的時候存在潛在的延遲,這樣就使得有可能存在一種競態條件,即允許一個線程首先看到不可變對象的一個值,一段時間之後看到的是一個不同的值。這種情況以前怎麼發生的呢?在JDK 1.4中的String實現里,這兒基本有三個重要的決定性欄位:對字元數組的引用、長度和描述字元串的開始數組的偏移量。String就是以這樣的方式在JDK 1.4中實現的,而不是只有字元數組,因此字元數組可以在多個String和StringBuffer對象之間共享,而不需要在每次創建一個String的時候都拷貝到一個新的字元數組里。假設有下邊的代碼:
String s1 = /usr/tmp;
String s2 = s1.substring(4); // /tmp
這種情況下,字元串s2將具有大小為4的長度和偏移量,但是它將和s1共享「/usr/tmp」裡面的同一字元數組,在String構造函數運行之前,Object的構造函數將用它們默認的值初始化所有的欄位,包括決定性的長度和偏移欄位。當String構造函數運行的時候,字元串長度和偏移量被設置成所需要的值。但是在舊的內存模型中,因為缺乏同步,有可能另一個線程會臨時地看到偏移量欄位具有初始默認值0,而後又看到正確的值4,結果是s2的值從「/usr」變成了「/tmp」,這並不是我們真正的初衷,這個問題就是原始JMM的第一個缺陷所在,因為在原始JMM模型裡面這是合理而且合法的,JDK 1.4以下的版本都允許這樣做。
2)問題2:重新排序的易失性和非易失性存儲
另一個主要領域是與volatile欄位的內存操作重新排序有關,這個領域中現有的JMM引起了一些比較混亂的結果。現有的JMM表明易失性的讀和寫是直接和主存打交道的,這樣避免了把值存儲到寄存器或者繞過處理器特定的緩存,這使得多個線程一般能看見一個給定變數最新的值。可是,結果是這種volatile定義並沒有最初想像中那樣如願以償,並且導致了volatile的重大混亂。為了在缺乏同步的情況下提供較好的性能,編譯器、運行時和緩存通常是允許進行內存的重新排序操作的,只要當前執行的線程分辨不出它們的區別。(這就是within-thread as-if-serial semantics[線程內似乎是串列]的解釋)但是,易失性的讀和寫是完全跨線程安排的,編譯器或緩存不能在彼此之間重新排序易失性的讀和寫。遺憾的是,通過參考普通變數的讀寫,JMM允許易失性的讀和寫被重排序,這樣以為著開發人員不能使用易失性標志作為操作已經完成的標志。比如:
Map configOptions;
char[] configText;
volatile boolean initialized = false;
// 線程1
configOptions = new HashMap();
configText = readConfigFile(filename);
processConfigOptions(configText,configOptions);
initialized = true;
// 線程2
while(!initialized)
sleep();
這里的思想是使用易失性變數initialized擔任守衛來表明一套別的操作已經完成了,這是一個很好的思想,但是不能在JMM下工作,因為舊的JMM允許非易失性的寫(比如寫到configOptions欄位,以及寫到由configOptions引用Map的欄位中)與易失性的寫一起重新排序,因此另外一個線程可能會看到initialized為true,但是對於configOptions欄位或它所引用的對象還沒有一個一致的或者說當前的針對內存的視圖變數,volatile的舊語義只承諾在讀和寫的變數的可見性,而不承諾其他變數,雖然這種方法更加有效的實現,但是結果會和我們設計之初大相徑庭。
❹ JAVA 里static 和volatile的區別
變數放在主存區上,使用該變數的每個線程,都將從主存區拷貝一份到自己的工作區上進行操作。
volatile, 聲明這個欄位易變(可能被多個線程使用),Java內存模型負責各個線程的工作區與主存區的該欄位的值保持同步,即一致性。
static, 聲明這個欄位是靜態的(可能被多個實例共享),在主存區上該類的所有實例的該欄位為同一個變數,即唯一性。
volatile, 聲明變數值的一致性;static,聲明變數的唯一性。
此外,volatile同步機制不同於synchronized, 前者是內存同步,後者不僅包含內存同步(一致性),且保證線程互斥(互斥性)。
static 只是聲明變數在主存上的唯一性,不能保證工作區與主存區變數值的一致性;除非變數的值是不可變的,即再加上final的修飾符,否則static聲明的變數,不是線程安全的。
下面摘自Java語言規范(Java Language Specification)的官方解釋:
1) If a field is declared static, there exists exactly one incarnation of the field, no matter how many instances (possibly zero) of the class may eventually be created.
2) A field may be declared volatile, in which case the Java Memory Model ensures that all threads see a consistent value for the variable。