java虚拟机10.内存模型与线程
如果讓計算機并發執行若干個運算任務就可以更充分地利用計算機處理器的效能,但是其中的復雜性是絕大多數的運算任務都不可能只靠處理器計算就能完成,處理器至少要與內存交互,如讀取運算數據、存儲運算結果等,這個I/O操作無法消除。
所以現代計算機通過加入一層讀寫速度盡可能接近處理器運算速度的告訴緩存來作為內存與處理器之間的緩沖:將運算需要使用的數據復制到緩存中,讓運算能快速進行,當運算結束后再從緩存同步回內存之中,這樣處理器就無需等待緩慢的內存讀寫了。
基于高速緩存的存儲交互很好地解決了處理器與內存的速度矛盾,但是引入了復雜度更高的一個問題:緩存一致性。
在多處理器系統中,每個處理器都有自己的高速緩存,而它們又共享同一主內存。當多個處理器的運算任務都涉及同一塊主內存區域時,將可能導致各自的緩存數據不一致,在將數據同步回主內存時以誰的緩存數據為準呢?為了解決一致性的問題,需要各個處理器訪問緩存時都遵循一些協議,在讀寫時要根據協議來進行操作。
因此內存模型可以理解為在特定的操作協議下,對特定的內存或高速緩存進行讀寫訪問的過程抽象。
除了增加高速緩存之外,為了使處理器內部的運算單元能盡量被充分利用,處理器可能會對輸入代碼進行亂序執行優化,處理器會在計算之后將亂序執行的結果重組,保證該結果與順序執行的結果是一致的,但并不保證程序中的各個語句計算的先后順序與輸入的代碼順序一致。因此,如果存在一個計算任務依賴另外一個計算任務的中間結果,那么其順序性并不能依靠代碼的先后順序來保證。與處理器的亂序執行優化器類似,java虛擬機的即時編譯器中也有類似的指令重排序優化。
Java內存模型
java虛擬機規范中試圖定義一種java內存模型來屏蔽掉各種硬件和操作系統的內存訪問差異,以實現讓java程序在各種平臺下都能達到一致的內存訪問效果。
java內存模型規定了所有的變量都存儲在主內存中,每條線程有自己的工作內存,線程的工作內存中保存了被該線程使用到的變量的主內存副本拷貝,線程對變量的所有操作都必須在工作內存中進行,而不能直接讀寫主內存中的變量。不同的線程之間也無法直接訪問對方工作內存中的變量,線程間變量值的傳遞均需要通過主內存來完成。
內存間交互操作
對于一個變量如何從主內存拷貝到工作內存、如何從工作內存同步回主內存的問題在java內存模型中定義了8種操作:
1.lock:作用于主內存的變量,它把一個變量標識為一條線程獨占的狀態。
2.unlock:作用于主內存的變量,它把一個處于鎖定狀態的變量釋放出來,釋放后的變量才可以被其他線程鎖定。
3.read:作用于主內存的變量,它把一個變量的值從主內存傳輸到線程的工作內存中,以便隨后的load動作使用。
4.load:作用于工作內存的變量,它把read操作從主內存中得到的變量值放入工作內存的變量副本中。
5.use:作用于工作內存的變量,它把工作內存中一個變量的值傳遞給執行引擎,每當虛擬機遇到一個需要使用到變量的值的字節碼指令時將會執行這個操作。
6.assign:作用于工作內存的變量,它把一個從執行引擎接收到的值賦給工作內存的變量,,每當虛擬機遇到一個給變量賦值的字節碼指令時將會執行這個操作。
7.store:作用于工作內存的變量,它把工作內存中一個變量的值傳送到主內存中,以便隨后的write操作使用。
8.write:作用于主內存的變量,它把store操作從工作內存中得到的變量的值寫入主內存的變量中。
如果要把一個變量從主內存復制到工作內存,需要順序的執行read和load操作,如果要把變量從工作內存同步回主內存,需要順序執行store和write。但java內存模型只要求兩個操作時順序執行,沒有保證是連續執行,即在兩個操作之間可插入執行其他指令。
所以java內存模型還規定了在執行上述8中操作時必須滿足的規則:
1.不允許read和load、store和write操作之一單獨出現,即不允許一個變量從主內存讀取了但工作內存不接受,或者從工作內存發起回寫了但主內存不接受的情況。
2.不允許一個線程丟棄它的最近的assign操作,即變量在工作內存中改變了之后必須把該變化同步回主內存。
3.不允許一個線程無原因地(沒有發生過任何assign操作)把數據從線程的工作內存同步回主內存中。
4.對一個變量實施use、store操作之前,必須先執行過load、assign操作。
5.一個變量在同一時刻只允許一條線程對其進行lock操作,但lock操作可以被同一條線程重復執行多次,多次執行lock后,只有執行相同次數的unlock操作,變量才會被解鎖。
6.如果對一個變量執行lock操作,那將會清空工作內存中此變量的值,在執行引擎使用這個變量前,需要重新執行load或assign操作初始化變量的值。
7.如果一個變量事先沒有被lock操作鎖定,那就不允許對它執行unlock操作,也不允許去unlock一個被其他線程鎖定的變量。
8.對一個變量執行unlock操作之前,必須先把此變量同步回主內存中,即store,write。
對于volatile型變量的特殊規則
java內存模型對volatile型變量專門定義了一些特殊的訪問規則:
假定T表示一個線程,V表示volatile變量,那么在進行read、load、use、assign、store和write操作時需要滿足如下規則:
1.只有當線程T對變量V執行的前一個動作是load的時候,線程T才能對變量V執行use動作;并且只有當線程T對變量V執行的后一個動作是use的時候,線程T才能對變量V執行load操作。線程T對變量V的use動作可認為是和線程T對變量V的load、read動作相關聯,必須連續一起出現。
這條規則要求在工作內存中,每次使用V前都必須先從主內存刷新最新的值,用于保證能看見其他線程對變量V所做的修改。
2.只有當線程T對變量V執行的前一個動作是assign的時候,線程T才能對變量V執行store動作;并且,只有當線程T對變量V執行的后一個動作是store的時候,線程T才能對變量V執行assign動作。線程T對變量V的assign動作可認為是和線程T對變量的store、write動作相關聯,必須連續一起出現。
這條規則要求在工作內存中,每次修改V后都必須立即同步回主內存中,用于保證其他線程可以看到自己對變量V的修改。
這兩個規則確保了volatile的可見性[1]。
所以volatile變量在各個線程的工作內存中不存在一致性問題,但java里面的運算并非原子操作,導致volatile變量在并發下一樣是不安全的(從主內存獲取變量值到交給執行引擎為止都是沒問題的,但是執行引擎在執行時不是原子操作,所以線程T1在修改變量值的時候,線程T2可以繼續獲取變量的舊值并交給自己的執行引擎,這樣線程T1的修改對線程T2就不可見,即出現并發問題)。
由于volatile變量只能保證可見性,在不符合以下兩條規則的運算場景中,仍然要通過加鎖來保證原子性。
1.運算結果不依賴變量的當前值,或者能夠確保只有單一的線程修改變量的值。
2.變量不需要與其他的狀態變量共同參與不變約束。
volatile變量的另一個語義是禁止指令重排序優化[2]。
普通的變量僅僅會保證在該方法的執行過程中所有依賴賦值結果的地方都能獲得正確的結果,而不能保證變量賦值操作的順序與程序代碼中的執行順序一致。因為在一個線程的方法執行過程中無法感知到這點,也就java內存模型中描述的:線程內表現為串行的語義。
eg:
Map configOptions;
char[] configText;
volatile boolean initialized = false;
//假設以下代碼在線程A中執行,讀取配置信息,讀取完后將initialized設置為true以通知其他線程配置可用。
configOptions = new HashMap();
configText = readConfigFile(fileName);
processConfigOptions(configText,configOptions);
initialized = true;
//假設以下代碼在線程B中執行,等待initialized為true,代表線程A已經把配置信息初始化完成。
while(!initialized){
?? sleep();
}
doSomethingWithConfig();
如果定義initialized變量時沒有使用volatile修飾,就可能由于指令重排序的優化,導致位于線程A中最后一句的代碼"initialized=true"被提前執行,這樣線程B中使用配置信息的代碼就可能出現錯誤。
volatile變量相當于在操作指令中的一個內存屏障,在指令重排序時不能把后面的指令重排序到內存屏障之前的位置。只有一個cpu訪問內存時,并不需要內存屏障;但如果有多個cpu訪問同一塊內存,且其中有一個在觀測另一個,就需要內存屏障來保證一致性了。
由于虛擬機對鎖實行的消除和優化,使得我們很難量化地認為volatile就會比synchronize快多少。volatile變量讀操作的性能消耗與普通變量幾乎沒有什么差別,但是寫操作則可能會慢一些,因為它需要在本地代碼中插入許多內存屏障指令來保證處理器不發生亂序執行。不過在大多數場景下volatile的總開銷仍然要比鎖低。我們在volatile與鎖之間選擇的唯一依據僅僅是volatile的語義能否滿足使用場景的需求。
原子性、可見性、有序性
java內存模型是圍繞著在并發過程中如何處理原子性、可見性和有序性這個三個特征來建立的。
原子性:
由java內存模型來直接保證的原子性變量操作包括read、load、assign、use、store、和write,我們大致可以認為基本數據類型的訪問讀寫是具備原子性的。對于更大范圍的原子性保證,java內存模型提供了lock和unlock操作來滿足這種需求。
可見性:
java內存模型是通過在變量修改后將新值同步回主內存,在變量讀取前從主內存刷新變量值這種依賴主內存作為傳遞介質的方式來實現可見性的,無論是普通變量還是volatile變量都是如此,普通變量與volatile變量的區別是,volatile的特殊規則保證了新值能立即同步到主內存, 以及每次使用前立即從主內存刷新。
除了volatile之外,java還有兩個關鍵字能實現可見性,即syncharonized和final。
同步塊的可見性由"對一個變量執行unlock操作之前,必須先把此變量同步回主內存中(store,write)"。而final的可見性是指,被final修飾的字段在構造器中一旦初始化完成,并且構造器沒有把"this"的引用傳遞出去,那在其他線程中就能看見final字段的值。
有序性:
java程序中天然的有序性可以總結為:如果在本線程內觀察,所有的操作都是有序的;如果在一個線程中觀察另一個線程,所有的操作都是無序的。前半句指'線程內表現為串行語義',后半句指'指令重排序和工作內存與主內存的同步延時。'
java提供了volatile和synchronized兩個關鍵字來保證線程之間操作的有序性,volatile本身就包含了禁止指令重排序的語義,而synchaonized則是有"一個變量在同一個時刻只允許一條線程對其進行lock操作"(lock之后只有自己能unlock,將自己的結果同步到主內存中)
先行發生規則
先行發生java內存模型中定義的兩項操作之間的偏序關系,如果說操作A先行發生于操作B,其實就是說在發生操作B之前,操作A產生的影響能被B觀察到。
java內存模型中有一些天然的先行發生關系,這些先行發生關系無須任何同步器協助就已經存在。
程序次序規則? 在一個線程內,按照代碼順序(控制流),寫在前面的操作先行發生于寫在后面的操作。
管程鎖定規則? 一個unlock操作先行發生于后面(時間上的)對同一個鎖的lock操作,這里強調的是同一個鎖。
volatile變量規則? 對一個volatile變量的寫操作先行發生于后面(時間上的)對這個變量的讀操作。
線程啟動規則? Thread對象的start()方法先行發生于此線程的每一個動作。
線程終止規則? 線程中的所有操作都先行發生于對此線程的終止檢測,可以通過Thread.join()方法結束。
線程中斷規則? 對線程interrupt()方法的調用先行發生于被中斷線程的代碼檢測到中斷事件的發生。
對象終結規則? 一個對象的初始化完成先行發生于它的finalize()方法的開始。
傳遞性? 如果操作A先行發生于操作B,操作B先行發生于操作C,那么操作A先行發生于操作C。
?
線程狀態
java定義了5種線程狀態,在任意一個時間點,一個線程有且只有其中一種狀態。
1.新建[New]:創建后尚未啟動的線程處于這種狀態。
2.運行[Runnable]:runnable包括了操作系統線程狀態中的Running和Ready,也就是處于此狀態的線程有可能正在執行,也有可能正在等待著CPU為它分配執行時間。
3.無限期等待[Waiting]:處于這種狀態的線程不會被分配CPU執行時間,他們要等待被其他線程顯示地喚醒:
.沒有設置Timeout參數的Object.wait()方法。
.沒有設置Timeout參數的Thread.join()方法。
.LockSupport.park()方法。
3.限期等待[Timed Waiting]:處于這種狀態的線程也不會被分配CPU執行時間,不過無須等待被其他線程顯示地喚醒,在一定時間之后它們會由系統自動喚醒:
.Thread.sleep()方法。
.設置了Timeout參數的Object.wait()方法。
.設置了Timeout參數的Thread.join()方法。
.LockSupport.parkNanos()方法。
.LockSupport.parkUntil()方法。
4.阻塞[blocked]:線程被阻塞了,“阻塞狀態”與“等待狀態”的區別是:“阻塞狀態”在等待著獲取到一個排他鎖,這個事件將在另外一個線程放棄這個鎖的時候發生,而“等待狀態”則是在等待一段時間,或者喚醒動作的發生。在程序等待進入同步區域的時候,線程將進入這種狀態。
5.結束[Terminated]:已終止線程的線程狀態,線程已經結束執行。
貼一張轉換圖:
?
Java語言中的線程安全
按照線程安全的“安全程度”由強至弱來排 序,可以將java語言中各種操作共享的數據分為以下5類:
不可變:只要一個不可變的對象被正確的構建出來(沒有發生this引用逃逸的情況),那其外部的可見狀態永遠也不會改變。String類就是一個典型的不可變對象,它的substring(),replace()和concat()方法都不會影響它原來的值,只會返回一個新構造的字符串對象。保證對象行為不影響自己狀態的途徑有很多種,其中最簡單的就是把對象中帶有狀態的變量都聲明為final,這樣在構造函數結束以后,它就是不可變的。 絕對線程安全:如vector容器,所有類方法都用synchronized修飾。 相對線程安全:通常意義上的線程安全,可以保證對象的單個操作是線程安全的,在調用的時候不需要做額外的保障措施,但對一些特定順序的連續調用,可能需要在調用端使用額外的同步手段來保證正確性。 線程兼容:指對象本身并不是線程安全的,但可以通過在調用端正確的使用同步手段來保證對象在并發環境中可以安全地使用。 線程對立:指無論調用端是否采取了同步措施,都無法再多線程環境中并發使用。鎖消除
鎖消除是指虛擬機即時編譯器在運行時,對一些代碼上要求同步,但是被檢測到不可能存在共享數據競爭的鎖進行消除。主要判斷依據來于逃逸分析的數據支持,如果判斷在一段代碼中,堆上的所有數據都不會逃逸出去從而被其他線程訪問到,那就可以把它們當做棧上數據對待,認為它們是線程私有的,同步加鎖自然就無須進行。
例如:public String condat(String s1,String s2,String s3){
? ? ? ? ? ? return s1+s2+s3;
???????? }
在jdk1.5之前,會轉化為StringBuffer對象的連續append()操作,在jdk1.5及之后的版本中,會轉化為StringBuilder對象的連續append()操作。
其實每個StringBuffer.append()方法中都有一個同步塊,鎖就是stringbuffer對象,但虛擬機觀察stringbuffer對象,發現它的動態作用域被限制在方法內部,就是說不會逃逸到方法外,因此會將鎖消除掉。
?
#筆記內容來自 《深入理解java虛擬機》
轉載于:https://www.cnblogs.com/shanhm1991/p/6403792.html
總結
以上是生活随笔為你收集整理的java虚拟机10.内存模型与线程的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: ccf--20140903--字符串匹配
- 下一篇: 264 I和IDR