一、Java內存模型 == JVM內存模型?
??很多人都會認為Java內存模型就是JVM內存模型,但實際上是錯的,Java內存模型是一個抽象的概念,描述了Java語言的一組規則和規范,JVM實際上也不僅僅支持運行Java代碼,還支持很多能在JVM上運行的語言如JRuby、Scale等,這是因為JRuby、Scale也有自己的語言規范,只要編譯出來的字節碼符合《Java虛擬機規范》,就可以在JVM上運行。
??JVM不關心代碼是用哪種編程語言寫的,只要編譯出來的指令碼符合JVM規范,那么就可以在JVM上運行,所有語言在JVM上的內存的結構都是一樣的,JVM上的內存模型圖如下。
??在JVM中,所有實例域、靜態域和數組元素存儲在堆內存中,堆內存在線程之間共享。局部變量、方法定義參數和異常處理器參數不會在線程之間共享,它們不會有內存可見性問題,也不會收內存模型的影響。
??《Java語言規范》中對Java內存模型的描述,主要是針對多線程程序的語義,包括當多個線程修改了共享內存中的值時,應該讀取到哪個值的規則,這些語義沒有規定如何執行多線程程序,相反它們描述了允許多線程程序的合法行為;所謂的“合法”,其實就是保證多線程對共享數據訪問的可見性和修改的安全性。
二、Java內存模型基礎
??在并發編程中,需要處理的兩個關鍵問題是:線程之前如何通信
和線程之間如何同步
。
1、通信
??通信
是指線程之間以何種機制來交換信息。在命令式編程中,線程之間的通信機制有兩種:共享內存
和消息傳遞
。
??在共享內存
的并發模型里,線程之間共享程序的公共狀態,線程之間通過寫-讀內存中的公共狀態
來隱式
進行通信。
??在消息傳遞
的并發模型里,線程之間沒有公共狀態,線程之間必須通過明確的發送消息
來顯式
進行通信。
2、同步
??同步
是指程序用于控制不同線程之間操作發生相對順序的機制。在共享內存
的并發模型里,同步是顯式
進行的,程序員必須顯式指定某個方法或某段代碼需要在線程之間互斥執行
;在消息傳遞
的并發模型里,由于消息的發送必須在消息的接收之前,因此同步是隱式
進行的。
??Java的并發采用的是共享內存模型,Java線程之間的通信總是隱式進行,整個通信過程對程序員完全透明。
3、Java內存模型的抽象
??Java線程之間的通信由Java內存模型(JMM)控制。JMM決定了一個線程對共享變量的寫入何時對另一個線程可見。從抽象的角度來看,JMM定義了線程與主內存之間的抽象關系:線程之間的共享變量存儲在主內存中,每一個線程都有一個自己私有的本地內存,本地內存中存儲了該變量以讀/寫共享變量的副本。本地內存是JMM的一個抽象概念,并不真實存在,只是為了幫助理解。
從上圖來看,如果線程A和線程B通信的話,要如下兩個步驟:
(1)線程A需要將本地內存A中的共享變量副本刷新到主內存中
(2)線程B去主內存讀取線程A之前已更新過的共享變量
【舉個例子】本地內存A和B有主內存共享變量X的副本。假設一開始時,這三個內存中X的值都是0。線程A正執行時,把更新后的X值(假設為1)臨時存放在自己的本地內存A中。當線程A和B需要通信時,線程A首先會把自己本地內存A中修改后的X值刷新到主內存去,此時主內存中的X值變為了1。隨后,線程B到主內存中讀取線程A更新后的共享變量X的值,此時線程B的本地內存的X值也變成了1。
??整體看來,這兩個步驟是指上是線程A在向線程B發送消息,而這個通信過程必須經過主內存。JMM通過控制主內存與每個線程的本地內存之間的交互,來為Java程序員提供內存可見性保證。
4、重排序
??在執行程序時為了提高性能,編譯器和處理器常常會對指令做重排序,重排序分三類:
(1)編譯器優化的重排序
??編譯器在不改變單線程程序語義的前提下,可以重新安排語句的執行順序。
(2)指令級并行的重排序
??現代處理器采用了指令級并行技術來將多條指令重疊執行。如果不存在數據依賴性,處理器可以改變語句對應及其指令的執行順序。
(3)內存系統的重排序
??由于處理器使用緩存和讀/寫緩沖區,這使得加載和存儲操作看上去可能是在亂序執行。
??從Java源代碼到最終實際執行的指令序列,會分別經歷下面三種重排序:
??注意,所有的重排都會遵循
as-if-serial語義
(詳見[Java多線程編程之四] CPU緩存和內存屏障),即重排后指令的對單線程來說跟重排前的指令的執行效果是一樣的,但是該語義不能保證程序指令在多線程環境下重排后的指令執行效果跟重排前一致,所以就會導致可見性問題,所謂可見性
問題,簡單地說就是某個線程修改了某個變量的值,但是對另外一個線程來說,它感知不到這種變化,當程序運行用到這個變量時,用的還是舊值,就會導致程序運行結果跟我們預料的大相庭徑。??上面的這些重排序都可能導致多線程程序出現內存可見性問題。
??對于編譯器,JMM的編譯器重排序規則會禁止特定類型的編譯器重排序(即可能導致程序可見性問題的重排序要禁止,但是不會禁止能優化程序執行效率并不影響程序執行結果正確性的重排序),如下所示:
??A和B的初始值都是0,對于線程1和線程2來說,重排序都不會影響單線程的執行效果,但是如果兩個線程并發操作A、B的值,則運行結果可能不一致,比如重排序前線程2給r1賦值的時候值為0,但是重排序后,賦值這個操作可能在線程1給B賦值1這步之后執行,此時對線程2來說賦值時B的值就是1了,而這種重排序就是JMM規范里要禁止的。
??對于處理器重排序,JMM的處理器重排序規則會要求Java編譯器在生成指令序列時,插入特定類型的內存屏障指令,通過內存屏障指令來禁止特定類型的處理器重排序(同樣不是所有的處理器重排序都要禁止)。
??JMM屬于語言級的內存模型,它確保在不同的編譯器和不同的處理器平臺之上,通過禁止特定類型的編譯器重排序和處理器重排序,為程序員提供一致的內存可見性保證。
處理器重排序
??現代的處理器使用寫緩沖區來臨時保存向內存寫入的數據。寫緩沖區可以保證指令流水線持續運行,它可以避免由于處理器停頓下來等待向內存寫入數據而產生的延遲。同時,通過以批處理的方式刷新寫緩沖區,以及合并寫緩沖區中對同一內存地址的多次寫,可以減少對內存總線的占用。雖然寫緩沖區有這么多好處,但每個處理器上的寫緩沖區,僅僅對它所在的處理器可見。這個特定會對內存操作的執行順序產生重要的影響:處理器對內存的讀/寫操作的執行順序,不一定與內存實際發生的讀/寫操作順序一致。
??示例如下:
??假設處理器A和處理器B按程序的順序并行執行內存訪問,最終卻可能得到
x = y = 0
。具體的原因如下圖所示:??處理器A和B同時把共享變量寫入在寫緩沖區中(A1、B1),然后再從內存中讀取另一個共享變量(A2、B2),最后才把自己寫緩沖區中保存的臟數據刷新到內存中(A3、B3)。當以這種時序執行時,程序就可以得到x = y = 0的效果。
??從內存操作實際發生的順序來看,直到處理器A執行A3來刷新自己的寫緩存去,寫操作A1才算真正執行了。雖然處理器A執行內存操作的順序為:A1 -> A2,但內存操作實際發生的順序卻是:A2 -> A1。此時,處理器A的內存操作順序被重排序了。
??這里的關鍵是,由于寫緩沖區僅對自己的處理器可見,它會導致處理器執行內存操作的順序可能會與內存實際的操作執行順序不一致。由于現代的處理器都會使用寫緩沖區,因此現代處理器都會允許對寫-讀操作重排序。
內存屏障指令
??為了保證內存可見性,Java編譯器在生成指令序列的適當位置會插入內存屏障指令來禁止特定類型的處理器重排序。
??處理器提供了兩個內存屏障指令:
(1)讀內存屏障:在指令前插入Load Barrier,可以讓高速緩存中的數據失效,強制重新從主內存加載數據,讓CPU緩存與主內存保持一致,避免緩存導致的一致性問題。
(2)寫內存屏障:在指令后插入Store Barrier,能讓寫入緩存中的最新數據更新寫入主內存,讓其他線程可見;當發生這種強制寫入主內存的顯式調用,CPU就不會處于性能優化考慮進行指令重排。
??程序進行讀寫時,指令執行順序可能有:讀寫,寫讀,讀讀,寫寫,所以JMM把內存屏障指令分為下列四類:
5、多線程編程中常見的問題
??對于新手,多線程編程不是個容易上手的技術,對于經驗豐富的老手,還時不時馬失前蹄,都會遇到很多問題,正因為這些問題,所以《Java語言規范》要提出一些規則范式來解決這些問題,常見的問題主要有:
- 所見非所得
- 無法用肉眼去檢測程序的準確性
- 不同的運行平臺有不同的表現
- 錯誤很難重現
??下面通過一個實例程序體會下這些問題:
public class Demo1Visibility {
int i = 0;
boolean isRunning = true;
public static void main(String[] args) throws InterruptedException {
Demo1Visibility demo = new Demo1Visibility();
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("hrer i am...");
while (demo.isRunning) {
demo.i++;
}
System.out.println(demo.i);
}
}).start();
Thread.sleep(3000L);
demo.isRunning = false;
System.out.println(demo.i);
System.out.println("shutdown...");
}
}
??程序的邏輯很簡單,有兩個線程:主線程和匿名線程,程序啟動時,主線程中會啟動匿名線程,接著主線程休眠3秒,由于demo.isRunning
的初始值為true
,因此匿名線程中的while
循環會不斷循環,直到主線程休眠結束后,將demo.isRunning
的值更新為false
,此時匿名線程的while
循環會結束,從而打印出demo.i
的值。
??但是程序運行后,發現運行結果并不像預料的那樣,程序并沒有打印出i
的值,并且一直處于運行狀態,很明顯這是因為匿名線程沒有感知到demo.isRunning
的變化,導致一直循環中,如下所示:
??如果運行用的是32位的JDK,并且設置程序的啟動VM參數
-client
(默認是-server
),則運行結果如下:??對于上面的程序,經過試驗可以知道,JDK、VM參數對程序運行的影響如下:
參數 | 32位JDK | 64位JDK |
---|---|---|
-server |
不打印i的值 | 不打印i的值 |
-client |
打印i的值 | 不打印i的值 |
??從上面的示例可以看出,程序運行的結果不一定如我們預料的(所見非所得),在不同版本的JDK下運行有差異(不同的運行平臺有不同的表現),使用不同的啟動參數-client/-server
有不同的效果,無法肉眼檢測程序的準確性,而這些問題就是《Java語言規范》提供的Java內存模型所要解決的問題,但是要注意Java內存模型并不會實際解決這些問題,更多的它是一種規范、規則,而JVM會去真正實現這些規則規范。
??對于上面代碼,在非32位JDK非-client
下,子線程都不能正常退出while
循環打印出i的值,我們結合上述講到的Java內存模型的示意圖來分析。
??首先demo
是一個對象,對象是存儲在堆內存中的,從運行時數據區的示意圖可知,堆內存是所有線程共享的區域;程序中有主線程和子線程兩個線程,每個線程在運行時JVM都會分配一塊線程私有的內存塊,我們稱之為線程工作區
,線程工作區就會存儲線程運行中需要的局部變量表、操作數棧、動態鏈接、返回地址等信息。主線程會修改demo.isRunning
的值為false
,會將修改內容寫入到共享堆內存中,而子線程會去讀取堆內存中的demo.isRunning
的值,來讓程序退出while
循環。
??當執行指令時,需要將其加載到CPU中,程序運行中需要存儲一些變量,這些變量會保存在RAM內存中,所以線程工作區既分布在CPU也分布在RAM內存中。從[Java多線程編程之四] CPU緩存和內存屏障可知,由于內存讀取寫入操作的數據遠遠跟不上CPU運行的速度,所以在CPU和內存中間,有個高速緩存,內存把數據加載到高速緩存中,供CPU讀取,當CPU要修改內存時,同樣是要先寫到高速緩存中,再由高速緩存同步到內存中,高速緩存協議又保證了內存中的數據被修改時同樣也會同步到其他線程的高速緩存中,如圖所示:
??從上一節對重排序的介紹可知,主線程去寫
data
時,不會馬上寫入內存中,而是先寫入緩存再寫入內存中,同步到內存中后,高速緩存協議會將修改同步到子線程的緩存中,這中間有一定的時延,所以子線程得過一段時間(這個時間其實很短,肉眼感受不到,但確實存在),按理說子線程應該在稍等一段時間后在while
循環里讀取到data
的最新值,然后退出循環,打印demo.i
的值,但是實際上卻沒有,這是怎么回事?
??這里又不得不再次提到上面說到的編譯優化重排序,在 [Java多線程編程之一] Java代碼是怎么運行起來的?看完這篇你就懂了!中提到Java的解釋執行和編譯執行,解釋執行指JVM在讀取字節碼執行時,是由執行引擎的解釋器逐條將字節碼翻譯成機器可識別的指令,編譯執行則是直接將一段字節碼翻譯成機器可以識別的指令碼。
??說起Java的編譯執行就不得不提到JIT編譯器(Just In Time Compiler)
,當Java程序中某個方法不斷被調用(比如遞歸)或者某段代碼不斷被循環(比如while(true)
)時,調用執行的頻率達到一定水平就會升級為熱點代碼,這時啟動JIT編譯
,直接將熱點代碼編譯成機器碼放到方法區中,當程序再次執行到這段熱點代碼時,直接從方法區中取機器指令執行,從而提升程序執行的效率,在JIT
編譯時,會進行指令重排做性能優化,指令重排不僅僅是對執行指令的重排,程序的邏輯可能也會發生改變,比如上面子線程執行的循環體,可能被優化成下面的形式:
boolean f = demo.isRunning;
if (f) {
while (true) {
i++;
}
}
??這相當于demo.isRunning
一開始就被緩存起來了,并且不會再次去讀取它的值,這就導致了主線程修改了demo.isRunning
時,子線程感受不到,所以一直在循環執行i++
,上面提到的運行VM參數-client
、-server
屬于JIT編譯
的參數,影響指令優化重排的行為,所以在32位JDK下,設置不同的參數會有不同的效果。
??問題的原因找到了,如何解決這個問題?很簡單,定義isRunning
時用volatile
修飾即可,volatile
有禁止指令重排的效果,如下所示:
??將使用
javap
編譯字節碼,可以看到對應的文本描述,從中可以看到對isRunning
的描述多了一個標志ACC_VOLATILE
??從官方文檔可以看出,加了
volatile
描述的字段是不能被緩存的,因此加了volatile
描述的字段,被多個不同線程訪問時,都是直接去內存中查找,而不會加一層緩存,這保證了可見性。6、Volatile關鍵字
??上面問題分析寫了一堆,最終卻出人意料地讓一個volatile
關鍵字輕松解決,volatile
有什么魔力?
??volatile
可以解決多線程環境中共享數據的可見性問題,即一個線程修改了共享變量時,其他線程馬上能夠感知到這種變化。
舉個例子:
public class VolatileTest {
volatile long a = 1L; // 使用volatile聲明的64位long型
public void set(long l) { // 單個volatile變量的寫
a = l;
}
public long get() { // 單個volatile變量的讀
return a;
}
public void getAndIncreament() {
a++; // 復合多個volatile變量的讀/寫
}
}
假設有多個線程分別調用上面程序的三個方法,則這個程序在語義上和下面的程序等價:
public class VolatileTest {
long a = 1L; // 64位的long型普通變量
public synchronized void set(long l) {
a = l; // 對單個普通變量的寫用同一個鎖同步
}
public synchronized long get() {
return a;
}
public void getAndIncreament() {
long temp = get();
temp += 1L;
set(temp);
}
}
??如上面示例程序所示,對一個volatile
變量的單個讀/寫操作,與對一個普通變量的讀/寫操作使用同一個鎖來同步,執行效果是相同的。
??鎖的語義決定了臨界區代碼的執行具有原子性,這意味著不管是什么類型的變量,只要它是volatile
變量,對該變量的讀寫就將具有原子性,但是這種原子性是對單個變量的操作而言的,如果是多個volatile
操作或類似于volatile++
這種復合操作,則其整體上不具有原子性。
??再回到5、多線程編程中的程序案例,如果對demo.isRunning
修飾了volatile
,禁止指令重排,JIT
不會優化出下面這種形式的代碼,程序讀寫時雖然也用到高速緩存,但是保證了多個線程對同個共享數據的同步可見。
boolean f = demo.isRunning;
if (f) {
while (true) {
i++;
}
}
??總結起來:volatile
變量自身具有下列特性:
-
可見性:對一個
volatile
變量的讀,總是能看到(任意線程)對這個volatile
變量最后的寫入。 -
原子性:對任意單個
volatile
變量的讀/寫具有原子性,但類似于volatile++
這種復合操作不具有原子性。
(1)volatile寫-讀的內存定義
- 當寫一個volatile時,JMM會把該線程對應的本地內存中的共享變量刷新到內存。
- 當讀一個volatile時,JMM會把該線程對應的本地內存中的共享變量置為無效,線程接下來將從主內存中讀取共享變量。
(2)volatile內存語義的實現
下面是JMM針對編譯器制定的volatile重排序規則表:
為了實現volatile的內存語義,編譯器在生成字節碼時,會在指令序列中插入內存屏障來禁止特定類型的處理器重排序。
下面是基于保守策略的JMM內存屏障插入策略:
- 在每個volatile寫操作的前面插入一個StoreStore屏障。
- 在每個volatile寫操作的后面插入一個StoreLoad屏障。
- 在每個volatile讀操作的前面插入一個LoadLoad屏障。
- 在每個volatile讀操作的后面插入一個LoadStore屏障。
下面是保守策略下,volatile寫操作插入內存屏障后生成的指令序列示意圖:
下面是保守策略下,volatile讀操作插入內存屏障后生成的指令序列示意圖:
上述volatile寫操作和讀操作的內存屏障插入策略非常保守。在實際執行時,只要不改變volatile寫 - 讀的內存語義,編譯器可以根據具體情況省略不必要的屏障。
三、Java內存模型中的一些語義和規則
1、as-if-serial語義
??不管怎么重排序(編譯器和處理器為了提高并行度),單線程程序的執行結果不能被改變,編譯器、runtime和處理器都必須遵循as-if-serial語義,也就是說,編譯器和處理器不會對存在數據依賴關系的操作做重排序。
2、Shaerd Variables定義
??可以在線程之間共享的內存稱為共享內存或堆內存。所有實例字段、靜態字段和數組元素都存儲在堆內存中,這些字段和數組都是共享變量。
??多個線程對共享變量的訪問操作中,如果至少有一個訪問時寫操作,那么對同一個變量的兩次訪問時沖突的,訪問順序的不一樣可能導致出現不同的結果。
3、線程間操作的定義
(1)線程間操作指:一個程序執行的操作可被其他線程感知或被其他線程直接影響。
(2)Java內存模型只描述線程間操作,不描述線程內操作,線程內操作按照線程內語義執行。
線程間操作有:
- 普通讀
- 普通寫
- volatile讀
- volatile寫
- Lock、Unlock:加鎖解鎖通常發生在多個線程對共享變量進行操作的同步。
- 線程的第一個和最后一個操作:簡單說就是一個線程的啟動和終止能被其他線程感知到。
- 外部操作:比如多個線程去訪問DB,DB是外部的資源,所以叫外部操作,也是線程間操作。
4、同步規則的定義
(1) 對volatile變量v的寫入,與所有其他線程后續對v的讀同步
(2) 對于監視器m的解鎖與所有后續操作對m的加鎖同步
這里有兩層語義:一層是加鎖解鎖操作是不能被重排序的;
另一層含義線程1拿到鎖做了一些操作(比如修改了某個變量的值),接著解鎖,接下來線程2拿到鎖,此時線程2是可以感知到線程1在持有鎖期間的操作。
(3)對于每個屬性寫入默認值(0,false, null)與每個線程對其進行的操作同步
??對象創建時,JVM會根據對象屬性類型等信息為其分配一塊內存,由于內存中可能存在臟數據,所以JVM在創建對象時,會根據其屬性類型初始化默認值,比如數字類型的初始化為0,布爾類型初始化為false,對象類型初始化為null,這個初始化的操作在線程訪問對象之前完成,確保訪問對象的線程不會看到“臟數據”(即沒初始化之前的內存亂碼)。
(4)啟動線程的操作與線程中的第一個操作同步
??這里同步的意思同樣有兩層含義,一層指線程要先被啟動才能執行線程里的run方法;一層指啟動線程的線程調用start()方法啟動了線程,這個啟動動作修改了被啟動的線程狀態,并且被啟動線程能感知到這種狀態的變化。
(5)線程T2的最后操作與線程T1發現線程T2已經結束同步(isAlive,join可以判斷線程是否終結)
??跟上面的規則差不多,就是T2線程操作結束時,線程狀態會修改成Terminated,這個線程狀態對其他線程是可見的。
(6)如果線程T1中斷了T2,那么線程T1的中斷操作與其他所有線程發現T2被中斷了同步,通過拋出InterruptedException異常,或者調用Thread.interrupted或Thread.isInterrupted
??線程T1調用t1.interrupted()來中斷線程T1,本質是修改T1線程對象的interrupted屬性的狀態值為true,這個狀態值對其他線程可見,其他線程發現線程T2中斷肯定在線程T1中斷T2之后發生
5、Happens-before先行發生原則
??happens-before關系用于描述兩個有沖突的動作之間的順序,如果一個action happends before另一個action,則第一個操作被第二個操作可見,JVM需要實現如下happens-before規則:
(1)某個線程中的每個動作都happens-before該線程中該動作后面的動作
??簡單地說就是代碼指令的順序執行
(2)某個管程上的unlock動作happens-before同一個管程上后續的lock操作
??先加鎖后解鎖,順序不可調整
(3)對某個volatile字段的寫操作happens-before每個后續對該volatile字段的讀操作
(4)在某個線程對象上調用start()方法happens-before該啟動線程中的任意動作
(5)如果在線程t1中成功執行了t2.join(),則t2中所有操作對t1可見
(6)如果某個動作a happens-before動作b,且b happens-before動作c,則有a happens-before c
6、final在JMM中的處理
Demo2Final
類的對象,線程1對對象屬性x、y的修改,只有x可以保證能被線程2讀取到正確的版本3,因為x被final
所修飾;但是y不一定能被讀取到正確的構造版本,線程2讀取y可能讀到的是0。
public class Demo2Final {
final int x;
int y;
static Demo2Final f;
public Demo2Final() {
x = 3;
y = 4;
}
static void writer() { f = new Demo2Final(); }
static void reader() {
if (f != null) {
int i = f.x; // 一定會讀到正確的構造版本
int j = f.y; // 可能會讀到默認值0
System.out.println("i = " + i + ", j = " + j);
}
}
public static void main(String[] args) throws InterruptedException {
// Thread1 writer
// Thread2 read
}
}
Demo3Final
對象,在構造函數中,被聲明為final
的屬性x先初始化,再用x來給y賦值,則y被初始化后的值也可以被線程2看到,而線程2可能依然看不到屬性c正確的構造版本。
public class Demo3Final {
final int x;
int y;
int c;
static Demo3Final f;
public Demo3Final() {
x = 3;
// #### 重點語句 ####
y = x; // 因為x被final修飾了,所以可讀到y的正確構造版本
c = 4;
}
static void writer() { f = new Demo3Final(); }
static void reader() {
if (f != null) {
int i = f.x;
int j = f.y;
int k = f.c;
System.out.println("i = " + i + ", j = " + j + ", k = " + k);
}
}
public static void main(String[] args) {
// Thread1 write
Demo3Final.writer();
// Thread2 read
}
}
7、Word Tearing字節處理
??有些處理器(尤其是早期的Alphas處理器)沒有提供寫單個字節的功能。在這樣的處理器上更新byte數組,若只是簡單地讀取整個內容,更新對應的字節,然后將整個內容再寫回內存,將是不合法的。
??這個問題有時候被稱為“字分裂(word tearing)”,更新單個字節有難度的處理器,就需要尋求其他方式來解決問題。因此,編程人員需要注意,盡量不要對byte[]中的元素進行重新賦值,更不要在多線程程序中這樣做。
如下圖所示,展示了字分類的問題:
??內存中存在數組[10, 2, 6, 9, 11, 23, 14],現在線程1要修改數組下標為4的元素值為10,線程2要修改數組下標為3的元素值為6。
??但是由于處理器無法寫單個字節,所以線程t1會拷貝整個數組的內容到線程內存中,對下標為4的元素進行賦值,再重新寫回內存中去,線程t2也是類似操作,但是這里存在一個問題,由于對數組進行寫操作是整個數組進行的,所以最后數組要么變成t1寫入的數組,要么變成t2寫入的數據,這兩個都會到時另一個線程的修改被抹除掉了,如下所示:
8、double和long的特殊處理
??由于《Java語言規范》的原因,對非volatile的double、long的單詞寫操作是分兩次來進行的,每次操作其中32位,這可能導致第一次寫入后,讀取的值是臟數據,第二次寫完成后,才能讀到正確值。但是現在的JVM大都針對這點進行了優化,使得對double、long類型數據的操作都能以整體64位進行,保持原子性,防止讀取的線程讀到臟數據。
??讀寫volatile修飾的long、double是原子性的。
??商業JVM不會存在這個問題,雖然規范沒要求實現原子性,但是考慮到實際應用,大部分都實現了原子性。
??《Java語言規范》中說道:建議程序員將共享的64位值(long、double)用volatile修飾或正確同步其程序以避免可能的復雜的情況。