Java基礎篇之Java虛擬機(二)----Java內存模型與線程; 線程安全與鎖優化;

點個關注,一起進步!

內容要點:

Java內存模型與線程;

線程安全與鎖優化;


 

​Java內存模型與線程

Java內存模型

 Java內存模型與JVM內存結構迷惑的的可以看下這個:

 Java基礎篇之Java虛擬機(一)

主內存與工作內存

 Java內存模型規定了所有的變量都存儲在主內存(Main Memory)中。每條線程還有自己的工作內存,線程的工作內存中保存了被該線程使用到的變量的主內存副本拷貝,線程對變量的所有操作(讀取、賦值等)都必須在工作內存中進行,而不能直接讀寫主內存中的變量。不同的線程之間也無法直接訪問對方工作內存中的變量,線程間變量值的傳遞均需要通過主 內存來完成,線程、主內存、工作內存三者的交互關係如下圖所示:

內存間交互操作

lock(鎖定):作用於主內存的變量,它把一個變量標識爲一條線程獨佔的狀態。 

unlock(解鎖):作用於主內存的變量,它把一個處於鎖定狀態的變量釋放出來,釋放 後的變量纔可以被其他線程鎖定。 

read(讀取):作用於主內存的變量,它把一個變量的值從主內存傳輸到線程的工作內 存中,以便隨後的load動作使用。

load(載入):作用於工作內存的變量,它把read操作從主內存中得到的變量值放入工作內存的變量副本中。 

use(使用):作用於工作內存的變量,它把工作內存中一個變量的值傳遞給執行引 擎,每當虛擬機遇到一個需要使用到變量的值的字節碼指令時將會執行這個操作。 

assign(賦值):作用於工作內存的變量,它把一個從執行引擎接收到的值賦給工作內存的變量,每當虛擬機遇到一個給變量賦值的字節碼指令時執行這個操作。 

store(存儲):作用於工作內存的變量,它把工作內存中一個變量的值傳送到主內存 中,以便隨後的write操作使用。

write(寫入):作用於主內存的變量,它把store操作從工作內存中得到的變量的值放入主內存的變量中。

內存間交互操作必須滿足以下規則:

不允許read和load、store和write操作之一單獨出現,即不允許一個變量從主內存讀取了但工作內存不接受,或者從工作內存發起回寫了但主內存不接受的情況出現。 

不允許一個線程丟棄它的最近的assign操作,即變量在工作內存中改變了之後必須把該變化同步回主內存。 

不允許一個線程無原因地(沒有發生過任何assign操作)把數據從線程的工作內存同步回主內存中。

不允許在工作內存中直接使用一個未被初始化 (load或assign)的變量,一個新的變量只能在主內存中“誕生”,就是對一個變量實施use、store操作之前,必須先執行過了assign和load操作。 

一個變量在同一個時刻只允許一條線程對其進行lock操作,但lock操作可以被同一條線程重複執行多次,多次執行lock後,只有執行相同次數的unlock操作,變量纔會被解鎖。 

如果對一個變量執行lock操作,那將會清空工作內存中此變量的值,在執行引擎使用這個變量前,需要重新執行load或assign操作初始化變量的值。

如果一個變量事先沒有被lock操作鎖定,那就不允許對它執行unlock操作,也不允許去 unlock一個被其他線程鎖定住的變量。 

對一個變量執行unlock操作之前,必須先把此變量同步回主內存中(執行store、write操 作)。 

 

volatile關鍵字

volatile是Java虛擬機提供的最輕量的同步機制

變量被volatile修飾具備兩個特性:

第一保證變量對所有線程可見,這裏的“可見性”是指當一條線程修改了這個變量的值,新值對於其他線程來說是可以立即得知的。而普通變量不能做到這一點,普通變量的值同步回主內存時間是不確定的。

但是“基於volatile變量的運算在併發下是安全的”這個結論是不完全正確的!

public class VolatileTest{     public static volatile int race=0;     public static void increase(){        race++;     }     private static final int THREADS_COUNT=20;     public static void main(String[]args){         Thread[]threads=new Thread[THREADS_COUNT];         for(int i=0;i<THREADS_COUNT;i++){             threads[i]=new Thread(new Runnable(){                 @Override public void run(){                     for(int i=0;i<10000;i++){ increase();                     }                 }             });             threads[i].start();         }         while(Thread.activeCount()>1) Thread.yield();         System.out.println(race);     } }

       這段代碼發起了20個線程,最後輸出的結果應該是200000,但是輸出的結果都不一樣,都是一個小於200000的數字, 這是爲什麼呢?

問題就出現在自增運算“race++”之中,volatile關鍵字保證了race的值在此時是正確的,但是在執行“race++”並不是原子操作,race+1然後再把值賦給race,如果完成了race+1,在賦值前另一個線程把race已經賦值+1啦,那麼兩個線程最終只+1。 

第二禁止指令重排序優化。普通變量僅僅能保證在該方法執行過程中,得到正確結果,但是不保證程序代碼的執行順序,volatile能保證之前代碼之前執行,之後代碼之後執行,但是不能保證之前以及之後一部分代碼具體的的執行順序。

 

原子性可見性和有序性

原子性(Atomicity):我們大致可以認爲基本數據類型的訪問讀寫是具備原子性的。對於更大範圍的原子操作,Java內存模型字節碼指令monitorenter和monitorexit來隱式地使用這兩個lock和unlock操作,這兩個字節碼指令反映到Java代碼中就是synchronized關鍵字。  

可見性(Visibility):可見性是指當一個線程修改了共享變量的值,其他線程能夠立即得知這個修改。Java內存模型是通過在變量修改後將新值同步回主內存,在變量讀取前從主內存刷新變量值來實現可見性的,普通變量,volatile變量都是這樣,volatile的特殊規則保證了新值能立即同步到主內存,以及每次使用前立即從主內存刷新。 除volatile之外,即synchronized和final也能實現可見性。同步塊的可見性是由“對一個變量執行unlock操作之前,必須先把此變量同步回主內存中,而final關鍵字的可見性是指:被final修飾的字段在構造器中一旦初始化完成,並且構造器沒有把“this”的引用傳遞出去,那在其他線程中就能看見final字段的值。

有序性(Ordering):Java程序中天然的有序性可以總結爲一句話:如果在本線程內觀察,所有的操作都是有序的;如果在一個線程中觀察另一個線程,所有的操作都是無序的。Java語言提供了volatile和synchronized兩個關鍵字來保證線程之間操作的有序性,volatile 關鍵字本身就包含了禁止指令重排序的語義,而synchronized則是由“一個變量在同一個時刻只允許一條線程對其進行lock操作”。

 

線程安全與鎖優化

線程安全

什麼是線程安全

“線程安全”有一個比較恰當的定義:“當多個線程訪問一個對象時,如果不用考慮這些線程在運行時環境下的調度和交替執行,也不需要進行額外的同步,或者在調用方進行任何其他的協調操作,調用這個對象的行爲都可以獲得正確的結果,那這個對象是線程安全的”。 

線程安全的實現

1,互斥同步(阻塞同步)

互斥同步是一種悲觀的併發策略,認爲不加鎖就一定會出問題。

同步是指在多個線程併發訪問共享數據時,保證共享數據在同一個時刻只被一個(或者是一些, 使用信號量的時候)線程使用。而互斥是實現同步的一種手段,臨界區(Critical Section)、互斥量(Mutex)和信號量(Semaphore)都是主要的互斥實現方式。

常用的synchronized關鍵字經過編譯之後,會在同步塊的前後形成monitorenter和monitorexit兩個字節碼,在執行monitorenter指令時,首先要嘗試獲取對象的鎖。

a,如果這個對象沒被鎖定,或者當前線程已經擁有了那個對象的鎖,把鎖的計數器加1,在執行monitorexit指令時會將鎖計數器減1,當計數器爲0時,鎖就被釋放。

b,如果獲取對象鎖失敗,那當前線程就要阻塞等待,直到對象鎖被另外一個線程釋放爲止。

但是sysnchronized是重量級鎖,濫用會極大影響本身業務代碼的執行效率,所以只在確定必要使用的情況下才去使用才更合理。

除了synchronized關鍵字,java.util.concurrent(簡稱JUC)包中的重入鎖 (ReentrantLock)也可以實現同步(CopyOnWriteArray在增刪改過程中就是利用的重入鎖實現同步的)相比synchronized,ReentrantLock;利用lock和unlock配合try,catch使用,並由以下特性,等待可中斷、可實現公平鎖,以及鎖可以綁定多個條件。

a,等待可中斷是指,當前持有鎖的線程長期不釋放鎖,等待鎖的線程可以選擇放棄等待。 

b,公平鎖是指,多個線程在等待同一個鎖時,必須按照申請鎖的時間順序來依次獲得鎖;而非公平鎖則不保證這一點,在鎖被釋放時,任何一個等待鎖的線程都有機會獲得鎖。synchronized中的鎖是非公平的,ReentrantLock默認情況下也是非公平的,但可以通過帶布爾值的構造函數要求使用公平鎖。 

c,鎖綁定多個條件是指,一個ReentrantLock對象可以同時綁定多個Condition對象,而在 synchronized中,鎖對象的wait()和notify()或notifyAll()方法可以實現一個隱含的條件,如果要和多於一個的條件關聯的時候,就不得不額外地添加一個鎖,而ReentrantLock只需要多次調用newCondition()方法。

 2,非阻塞同步   

非阻塞同步是一種基於衝突檢測的樂觀併發策略,先進行操作,衝突在進行彌補。

操作和衝突檢測,需要基於硬件指令集的發展,來保證其原子性。

3,無同步方案

可重入代碼

線程本地存儲

鎖優化

1,自旋鎖

爲了不是每次等待鎖的線程都去掛起,1.4.2中引入自旋鎖,只不過默認是關閉的,可以使用-XX:+UseSpinning 參數來開啓,在JDK 1.6中就已經改爲默認開啓了,主要目的是可能會有線程等待鎖時間比較短,讓線程完成一個忙循環(自旋),不過問題在於如果線程長時間不釋放鎖,一直自旋不僅浪費處理器資源還對完成任務沒有任何幫助。自旋次數的默認值是10次,參數-XX:PreBlockSpin來更,在JDK 1.6中引入了自適應的自旋鎖,根據前一次自旋獲取鎖的成功率來決定自旋時間,比如上次通過自選獲取到了鎖,那麼這次也大機率會獲得,所以自旋時間可能會比較長,相反會比較短。

2,鎖消除

鎖消除是指虛擬機即時編譯器在運行時,對一些代碼上要求同步,但是被檢測到不可能存在共享數據競爭的鎖進行消除。鎖消除的主要判定依據來源於逃逸分析的數據支持,如果判斷在一段代碼中,堆上的所有數據都不會逃逸出去被其他線程訪問到,那就可以把它們當做棧上數據對待,認爲它們是線程私有的,同步加鎖自然就無須進行。

3,鎖粗化

 原則上,同步快的左右範圍要儘量小,但是如果一系列聯繫操作,都對同一對象反覆加鎖和解鎖,甚至加鎖操作在循環體內,頻繁的互斥同步也會導致不必要的性能損耗,虛擬機檢測到後對加鎖同步範圍進行擴充,達到只加一次鎖的目的。

4,輕量級鎖

是在沒有多線程競爭的前提下,減少傳統的重量級鎖使用操作系統互斥量產生的性能消耗。如果沒有競爭,輕量級鎖使用CAS操作避免了使用互斥 量的開銷,但如果存在鎖競爭,除了互斥量的開銷外,還額外發生了CAS操作,因此在有競爭的情況下,輕量級鎖會比傳統的重量級鎖更慢。  

5,偏向鎖

如果說輕量級鎖是在無競爭的情況下使用CAS操作去消除同步使用的互斥量,那偏向鎖就是在無競爭的情況下把整個同步都消除掉,連CAS操作都 不做了,偏向鎖會偏向於第一個獲得它的線程,如果在接下來的執行過程中,該鎖沒有被其他的線程獲取,則持有偏向鎖的線程將永遠不需要再進行同步。

長按關注,一起進步!

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章