內容要點:java
Java內存模型與線程;面試
線程安全與鎖優化;安全
Java內存模型與JVM內存結構迷惑的的能夠看下這個:多線程
主內存與工做內存ide
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 Volatile{
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操做都 不作了,偏向鎖會偏向於第一個得到它的線程,若是在接下來的執行過程當中,該鎖沒有被其餘的線程獲取,則持有偏向鎖的線程將永遠不須要再進行同步。
這次記錄也是本身學習記錄的過程,筆者能力有限若是您發現有不足或者錯誤之處,敬請雅正,不捨賜教。
若是你也在學習或者複習,能夠關注個人公衆號【Java成長錄】,有系統的學習規劃路線,每次學習記錄文章。
Java 虛擬機面試題全面解析 - 做業部落 Cmd Markdown 編輯閱讀器
周志明. 深刻理解 Java 虛擬機 [M]. 機械工業出版社, 2011.