java內存模型等
⑴ java內存模型和Java內存區域的區別和聯系
內存模型就是各個區域的職責劃分,說起來是一個事情。
Java虛擬機在執行Java程序的過程中會把它所管理的內存劃分為若干個不同的數據區.這些區域有各自的用途,以及創建和銷毀時間,有的區域隨著虛擬機進程的啟動而存在,有的依賴用戶線程的啟動和結束而建立和銷毀,概括來說Java虛擬機包括以下幾個運行時數據區域,程序計數器,Java虛擬機棧,本地方法棧,Java堆,方法區,運行時常量池,(直接內存)
⑵ java的內存模型是怎麼設計的
先來畫一個簡圖,java內存模型可以這樣分類:
packagecom.wyq.Memory;
publicclassPerson{
publicStringname;//成員變數,存放在棧中
publicstaticintage;//static類型的變數,存放在靜態變數區
publicstaticfinalStringsex="女";//常量在常量池中
publicPerson(Stringname){
this.name=name;
}
publicvoidwalk(Personperson){//傳入一個person引用,存放在棧中
intstep=5;//局部變數,基本數據類型,存放在棧中
System.out.println(person.name+"跑了"+step+"步");
}
publicstaticvoidmain(String[]args){
Personperson=newPerson("樓主");//由構造器new一個person對象,存放在堆中
person.walk(person);
}
}
//整個代碼在代碼區中
⑶ java內存模型有哪些
JMM保證:如果程序是正確同步的,程序的執行將具有順序一致性 。
JMM設計
從JMM設計者的角度來說,在設計JMM時,需要考慮兩個關鍵因素:
(01) 程序員對內存模型的使用。程序員希望內存模型易於理解,易於編程。程序員希望基於一個強內存模型(程序盡可能的順序執行)來編寫代碼。
(02) 編譯器和處理器對內存模型的實現。編譯器和處理器希望內存模型對它們的束縛越少越好,這樣它們就可以做盡可能多的優化(對程序重排序,做盡可能多的並發)來提高性能。編譯器和處理器希望實現一個弱內存模型。
JMM設計就需要在這兩者之間作出協調。JMM對程序採取了不同的策略:
(01) 對於會改變程序執行結果的重排序,JMM要求編譯器和處理器必須禁止這種重排序。
(02) 對於不會改變程序執行結果的重排序,JMM對編譯器和處理器不作要求(JMM允許這種重排序)。
⑷ java內存模型的介紹
Java平台自動集成了線程以及多處理器技術,這種集成程度比Java以前誕生的計算機語言要厲害很多,該語言針對多種異構平台的平台獨立性而使用的多線程技術支持也是具有開拓性的一面,有時候在開發Java同步和線程安全要求很嚴格的程序時,往往容易混淆的一個概念就是內存模型。究竟什麼是內存模型?內存模型描述了程序中各個變數(實例域、靜態域和數組元素)之間的關系,以及在實際計算機系統中將變數存儲到內存和從內存中取出變數這樣的底層細節,對象最終是存儲在內存裡面的,這點沒有錯,但是編譯器、運行庫、處理器或者系統緩存可以有特權在變數指定內存位置存儲或者取出變數的值。【JMM】(Java Memory Model的縮寫)允許編譯器和緩存以數據在處理器特定的緩存(或寄存器)和主存之間移動的次序擁有重要的特權,除非程序員使用了final或synchronized明確請求了某些可見性的保證。
⑸ 什麼是Java內存模型
1.java內存模型簡稱JMM
2.通俗點講就是指你代碼中寫的那些內容在內存中是怎麼存儲的
3.比如說 Student stu = new Student();int a = 5; 你新建了一個學生類,建立了一個變數a,這些內容在內存中是咋存儲的,
4.大的來說JMM分為堆棧,一般情況下學生類分配在堆,變數a分配在棧,
⑹ java內存模型和運行時的數據區域的區別
(1).程序計數器:
是一塊較小的內存空間,其作用可以看作是當前線程所執行的位元組碼的行號指示器,位元組碼解析器工作時通過改變程序計數器的值來選取下一條需要執行的位元組碼指令。程序的分支、循環、跳轉、異常處理以及線程恢復等基礎功能都是依賴程序計數器來完成。
Java虛擬機的多線程是通過線程輪流切換並分配處理器執行時間片來實現,在任何一個時刻,一個處理器只會執行一條線程指令,因此,為了確保線程切換之後能恢復到正確的執行位置,每條線程都需要一個獨立的程序計數器,因此程序計數器是線程私有的內存。
程序計數器是java虛擬機中唯一一個沒有規定任何內存溢出OutOfMemoryError的內存區域。
(2).java虛擬機棧:
Java虛擬機棧也是線程私有的,它的生命周期與線程相同。虛擬機棧描述的是java方法執行的內存模型:每個方法被執行時都會同時創建一個棧幀用於存放局部變數表、操作數棧、動態連接和方法出口等信息。每個方法被調用直至執行完成過程,就對應著一個棧幀在虛擬機中從入棧到出棧的過程。
Java虛擬機棧的局部變數表存放了編譯器可知的8種java基本類型數據、對象引用(注意不是對象實例本身)、方法返回地址returnAddress。
Java虛擬機棧的局部變數表空間單位是槽(Slot),其中64位長度的double和long類型會佔用兩個slot,其餘的數據類型只佔用一個slot。局部變數表所需內存空間在編譯期間完成分配,當進入一個方法時,該方法需要在幀中分配多大的局部變數空間是完全確定的,在方法運行期間不會改變局部變數表的大小。
Java虛擬機棧有兩種異常狀況:如果線程請求的棧深度大於虛擬機所允許的最大深度時,拋出StackOverflowError異常;如果虛擬機棧可以動態擴展,當擴展時無法申請到足夠內存時會拋出OutOfMemoryError異常。
(3).本地方法棧:
本地方法棧與java虛擬機棧作用非常類似,其區別是:java虛擬機棧是為虛擬機執行java方法服務,而本地方法棧是為虛擬機調用的操作系統本地方法服務。
Java虛擬機規范沒有對本地方法棧的實現和數據結構做強制規定,Sun HotSpot虛擬機直接把java虛擬機棧和本地方法棧合二為一。
與java虛擬機棧類似,本地方法棧也會拋出StackOverflowError異常和OutOfMemoryError異常。
(4).堆:
堆是java虛擬機所管理的內存區域中最大一塊,java堆是被所有線程所共享的一塊內存區域,在java虛擬機啟動時創建,堆內存的唯一目的就是存放對象實例。幾乎所有的對象實例都是在堆分配內存。
Java堆是垃圾收集器管理的主要區域,從垃圾回收的角度看,由於現在的垃圾收集器基本都採用的是分代收集演算法,因此java堆還可以初步細分為新生代和年老代。
Java虛擬機規范規定,堆可以處於物理上不連續的內存空間中,只要邏輯上是連續的即可。在實現上即可以是固定大小的,也可以是可動態擴展的。如果在堆中沒有內存完成實例分配,並且堆大小也無法在擴展時,將會拋出OutOfMemoryError異常。
(5).方法區:
方法區與堆一樣,是被各個線程共享的內存區域,它用於存儲已被虛擬機載入的類信息、常量、靜態變數、即時編譯後的代碼等數據。雖然java虛擬機規范把方法區描述為堆的一個邏輯部分,但是方法區卻有一個別名叫Non-Heap(非堆)。
Sun HotSpot虛擬機把方法區叫永久代(Permanent Generation),方法區中最重要的部分是運行時常量池。Class文件中除了有類的版本、欄位、方法、介面等描述信息外,還有一項信息是常量池,用於存放編譯期生成的各種字面變數、符號引用、直接引用等,這些內容將在類載入後存放到方法區的運行時常量池中,另外在運行期間也可以將新的常量存放到常量池中,如String的intern()方法。
方法區和運行時常量池在無法滿足內存分配時,也會拋出OutOfMemoryError異常。
(6).直接內存:
直接內存並不是java虛擬機運行時數據區的一部分,也不是java虛擬機規范中定義的內存區域,但是在java開發中還是會使用到。
JDK1.4中新引入的NIO(new I/O),引入了一種基於通道(Channel)和緩沖區(Buffer)的I/O方式,可以使用操作系統本地方法庫直接分配堆外內存,然後通過一個存儲在java堆裡面的DirectByteBuffer對象作為堆外直接內存的引用進行操作,避免了java堆內存和本地直接內存間的數據拷貝,可以顯著提高性能。
雖然直接內存並不直接收到java虛擬機內存影響,但是如果java虛擬機各個內存區域總和大於物理內存限制,從而導致直接內存不足,動態擴展時也會拋出OutOfMemoryError異常。
java虛擬機內存結構中的程序計數器、虛擬機棧和本地方法棧這三個區域隨線程創建而生,隨線程銷毀而滅,因此這三個區域的內存分配和回收是確定的,java垃圾收集器重點關注的是java虛擬機的堆內存和方法區內存。
⑺ 再有人問你Java內存模型是什麼,就把這篇文
你說的應該是主存和工作內存吧,主存是公共空間,基本可以類比為虛擬機模型中的堆,對象創建好了都是在主存里,所有線程都可以訪問,工作內存是線程的私有內存,只有本線程可以訪問,如果線程要操作主存中的某個對象,必須從主存中拷貝到工作內存,在對工作內存中的副本進行操作,操作後再寫入主存,而不能對主存的對象直接操作
⑻ 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內存模型FAQ 什麼是內存模型
內存模型描述的是程序中各變數(實例域、靜態域和數組元素)之間的關系,以及在實際計算機系統中將變數存儲到內存和從內存取出變數這樣的低層細節。對象最終存儲在內存中,但編譯器、運行庫、處理器或緩存可以有特權定時地在變數的指定內存位置存入或取出變數值。例如,編譯器為了優化一個循環索引變數,可能會選擇把它存儲到一個寄存器中,或者緩存會延遲到一個更適合的時間,才把一個新的變數值存入主存。所有的這些優化是為了幫助實現更高的性能,通常這對於用戶來說是透明的,但是對多處理系統來說,這些復雜的事情可能有時會完全顯現出來。
JMM 允許編譯器和緩存以數據在處理器特定的緩存(或寄存器)和主存之間移動的次序擁有重要的特權,除非程序員已經使用 synchronized 或 final 明確地請求了某些可見性保證。這意味著在缺乏同步的情況下,從不同的線程角度來看,內存的操作是以不同的次序發生的。
與之相對應地,像 C 和 C++ 這些語言就沒有顯示的內存模型 —— 但 C 語言程序繼承了執行程序處理器的內存模型(盡管一個給定體系結構的編譯器可能知道有關底層處理器的內存模型的一些情況,並且保持一致性的一部分責任也落到了該編譯器的頭上)。這意味著並發的 C 語言程序可以在一個,而不能在另一個,處理器體系結構上正確地運行。雖然一開始 JMM 會有些混亂,但這有個很大的好處 —— 根據 JMM 而被正確同步的程序能正確地運行在任何支持 Java 的平台上。
⑽ 到底什麼是JAVA內存模型
了解java內存模型對深入了解jvm有很多好處。 JMM通過控制主內存與每個線程的本地內存之間的交互,來為開發者提供內存可見性保證。