一、前言
本來計划將ConcurrentHashMap和HashMap對比着來說下,奈何看的源碼有點懵逼,我在思考思考,等等有個清晰的思路在搞起來,我們先來談一下synchronized,主要從用法,JVM兩個方面來說一下;
二、用法
要談用法,首先要明白什么時候我們需要使用,在並發編程中照成線程安全問題主要有2點原因:1.共享資源;2.同時操作;這個時候我們就需要保證在同一時刻只能允許一個線程訪問或者操作共享資源,Java中給我們提供鎖,這種方式來實現同一時刻只有一個線程可以操作共享資源,另外同一時刻訪問改資源的其他線程處於等待狀態,這個鎖也叫做互斥鎖,保證同一時刻只有一個線程訪問,同時保證了內存可見性;
接下來也引入我們的重點 synchronized:
1.修飾靜態方法
synchronized修飾靜態方法的時候,鎖的是當前類的class對象鎖,看如下代碼

public class SyncClass implements Runnable { static int i=0; //鎖的是當前類 public static synchronized void test(){ i++; } public void run() { for (int j=0;j<1000000;j++){ test(); } } public static void main(String[] args) throws InterruptedException { Thread thread1=new Thread(new SyncClass()); Thread thread2=new Thread(new SyncClass()); thread1.start(); thread2.start(); thread1.join(); thread2.join(); System.out.println(i); } } public class SyncClass implements Runnable { static int i=0; //當前實例 public synchronized void test(){ i++; } public void run() { for (int j=0;j<1000000;j++){ test(); } } public static void main(String[] args) throws InterruptedException { Thread thread1=new Thread(new SyncClass()); Thread thread2=new Thread(new SyncClass()); thread1.start(); thread2.start(); thread1.join(); thread2.join(); System.out.println(i); } }
上面的方法,執行一下會發現有很大的不同,這里我們進行分析,在SyncClass中,i變量屬於共享變量,當在多線程情況下調用test()靜態方法修飾的同步方法的時候,沒有發生因為資源共享而導致的i輸出的時候小於2000000,而當調用非靜態的修飾的同步方法的時候,發生因為線程共享資源導致和自己預期不一樣的值,所以這個時候我們就會發現,當synchronized修飾靜態方法時候是鎖的當前類,修飾非靜態的類方法的時候是修飾的當前實例,當然這個還需要下面在證明一下。
2.修飾非靜態的方法
synchronized修飾非靜態方法的時候,鎖的是當對象的實例,看如下代碼

public class SyncClass implements Runnable { static int i=0; public synchronized void test(){ i++; } public void run() { for (int j=0;j<1000000;j++){ test(); } } public static void main(String[] args) throws InterruptedException { SyncClass insatance=new SyncClass(); Thread thread1=new Thread(insatance); Thread thread2=new Thread(insatance); thread1.start(); thread2.start(); thread1.join(); thread2.join(); System.out.println(i); } }
上面的方法,當我們傳入instance的時候,沒有發生因為共享資源而導致i輸出的時候小於2000000,與上面另外一種傳入類的修飾非靜態的方法做比較,這個時候我們可以得出,當synchronized修飾非靜態的方法的時候鎖的對象是當前類的實例;
3.修飾代碼塊
這個就是提升鎖的效率,沒必要每次對方法進行同步操作,看如下代碼

public class SyncClass implements Runnable { static SyncClass instance=new SyncClass(); static int i=0; public void test(){ i++; } public void run() { //給定的實例 synchronized (instance){ for (int j=0;j<1000000;j++){ test(); } } //當前實例 // synchronized (this){ // for (int j=0;j<1000000;j++){ // test(); // } // } //當前類 // synchronized (SyncClass.class){ // for (int j=0;j<1000000;j++){ // test(); // } // } } public static void main(String[] args) throws InterruptedException { SyncClass insatance=new SyncClass(); Thread thread1=new Thread(insatance); Thread thread2=new Thread(insatance); thread1.start(); thread2.start(); thread1.join(); thread2.join(); System.out.println(i); } }
用法如上,主要有3種情況,我上面進行了展示,1.鎖住的是特定的對象,2.鎖住的是當前實例,3.鎖住的當前類
三、推敲原理
synchronized是通過互斥來保證並發的正確性的問題,synchronized經過編譯后,會在同步塊前后形成monitorenter和monitorexit這兩個字節碼,其中monitorenter指令指向同步代碼塊的開始位置,monitorexit指令則指明同步代碼塊的結束位置,當執行monitorenter指令的時,首先嘗試獲取對象的鎖,如果當前對象沒有被鎖定,或者當前對象已經擁有對象的鎖,那么就把鎖的計數器加1,相應的在執行monitorexit的時候會將鎖的計數器減1,當計數器為0的時候,鎖就被釋放,如果獲取鎖的對象失敗,則當前線程就要阻塞等待,直到對象鎖被另外一個線程鎖釋放為止----以上來自深入Java虛擬機一書;
這里我們思考下當我們拿到這個場景的時候如何去做,我們要搞清3個問題也就能實現上面場景了:
1.計數器問題;2.對象狀態問題
這2個問題處理起來比較簡單,就是在對象里面增加一個count屬性去記錄加鎖和解鎖以后的數量就可以,另外狀態問題也隨之處理完成,個數數量為0的時候處於未鎖定,大於0的時候處於鎖定狀態;
3.線程問題
首先線程問題有兩種狀態,一種處於正在等待鎖的狀態,另外一種處於等待狀態,分析清楚就很簡單了,我們可以用2個隊列來處理這個問題,隊列里只要能記錄線程Id就可以,保證能知道我們要喚醒那個線程就可以,當等待狀態的隊列為空的時候對象也處與為加鎖狀態,線程計數器也為0;
分析到這里我相信大家也比較清晰了,實現我不寫了,這種問題考慮下就好了,思路為王,Java這個是通過C++去實現的,基本思路也是如此,我們重點主要來看下Java對象頭包括那些,這個地方關系鎖優化等等方面吧,只要明白這塊的東西我相信很容易徹底掌握好synchronized;
Java對象頭
synchronized用的鎖是存在Java對象頭里的,什么是Java的對象頭,HotSpot虛擬機的對象的頭分為兩個部分,第一部分用於存儲對象自身運行時的數據,包括哈希碼,GC分代年齡等,官方稱為Mark Word;另外一部分用於存儲指向方法區的對象類型的指針,虛擬機通過這個指針來確定這個對象是哪個類的實例,這塊在之前在介紹虛擬機的時候有說過;
Mark Word
Mark Word在32位的HotSpot虛擬機種對象處理未鎖定狀態下的默認存儲結構:
Mark Word 被設計成為一個非固定的數據結構,以便存儲更多有效的數據,它會根據對象本身的狀態復用自己的存儲空間,如32位JVM下,除了上述列出的Mark Word默認存儲結構外,還有如下可能變化的結構:
分析完對象頭以后,我們返回到上面考慮那個場景,不要去管輕量級鎖和偏向鎖,這些都是JVM做的優化,這個等等再談,將重點放到重量級鎖上面來,其實我們剛剛考慮對象的設計也就是重量級鎖指向的指針monitor對象設計,當然我們考慮可能沒有那全面,但是該有的重點都有了,當我們創建對象的時候都會創建與monitor的關聯關系,當monitor創建以后生命周期與對象創建的生命周期是一樣的,同生共死,相信到這里你已經能徹底明白重量級鎖的實現原理了,要是在不明白去看下反編譯的源碼,在考慮考慮我設計時候考慮的3個問題,我想必然會融匯貫通;
四、鎖優化
jdk1.6對鎖的實現引入了大量的優化,如自旋鎖、適應性自旋鎖、鎖消除、鎖粗化、偏向鎖、輕量級鎖等技術來減少鎖操作的開銷。
鎖主要存在4種狀態,依次是:無鎖狀態、偏向鎖狀態、輕量級鎖狀態、重量級鎖狀態,他們會隨着競爭的激烈而逐漸升級。注意鎖可以升級不可降級,這種策略是為了提高獲得鎖和釋放鎖的效率。
1.自旋鎖
線程之間的切換主要是靠CPU進行處理的,來回切換肯定會給服務器照成很大壓力。可是我們有些時候鎖的操作並不會持續很久,這個時候就沒有必要進行線程的來回切換,針對於這種狀況就引入了自旋鎖;什么是自旋?就是讓線程循環等待,看在一定時間內持有鎖的線程是否釋放,當然這個是建立在多核的基礎上的,一個需要執行當前線程的操作,另外一個需要判斷線程是否執行完成,自旋避免的線程之間切換帶來的性能消耗,但是他需要占用處理器的時間,這個時候如果持有鎖的線程執行時間很長的話,在性能則是一種浪費,所以自旋等待必須要有一個度;自旋鎖在JDK 1.4.2中引入,默認關閉,但是可以使用-XX:+UseSpinning開啟,在JDK1.6中默認開啟。同時自旋的默認次數為10次,可以通過參數-XX:PreBlockSpin來調整;JDK1.6以后引入自適應自旋鎖,變得智能化,可以根據運行狀況自行進行判斷;
2.鎖消除
這個也是虛擬機自行判斷的,主要是針對不存在競爭的資源進行的優化,判斷的依據主要是依據是逃逸分析的數據支持,這里簡單介紹一下,逃逸分析就是分析對象作用域,這個我想用點大白話說,就是不是在本類內部使用的對象,虛擬機做的就是如果一個變量無法被其他線程所訪問到,那么這個變量就不會存在競爭,就可以對這個變量修飾的同步進行鎖的消除;
3.鎖粗化
這個也是虛擬機自行判斷的,多個連續的加鎖、解鎖操作連接在一起,擴展成一個范圍更大的鎖;
4.輕量級鎖
輕量級鎖也還是為了解決重量級鎖線程切換照成性能的問題,主要是通過CAS的操作實現,接下來主要分析執行流程:
加鎖流程:
1).判斷當前對象是否處於無鎖狀態,如果是無鎖狀態,JVM會在當前線程的棧幀中建立一個名為鎖記錄(Lock Record)的空間,用於存儲鎖對象目前的Mark Word的拷貝(官方把這份拷貝加了一個Displaced前綴,即Displaced Mark Word);
2).JVM利用CAS操作嘗試將對象的Mark Word更新為指向Lock Record的指正,如果成功表示競爭到鎖,則將鎖標志位變成00(表示此對象處於輕量級鎖狀態),執行同步操作;
3).判斷當前對象的Mark Word是否指向當前線程的棧幀,如果是則表示當前線程已經持有當前對象的鎖,則直接執行同步代碼塊;否則只能說明該鎖對象已經被其他線程搶占了,這時輕量級鎖需要膨脹為重量級鎖,鎖標志位變成10,后面等待的線程將會進入阻塞狀態;
釋放鎖流程:
1).如果對象的Mark Word還是指向的線程的鎖,那么就取出在獲取輕量級鎖保存在Displaced Mark Word中的數據;
2).用CAS操作將取出的數據替換當前對象的Mark Word中,如果成功,則說明釋放鎖成功;
3). 如果CAS操作替換失敗,說明有其他線程嘗試獲取該鎖,則需要在釋放鎖的同時需要喚醒被掛起的線程;
這樣子介紹我感覺大家還是會有疑惑,現在我們A和B線程為例,再來說一下這個流程:
1).當A和B線程同時進入無所狀態的時候,這個時候都會進行復制Mark Word操作;
2).當進行CAS操作的時候只能保證有一個線程能執行成功,假設A線程執行成功這個時候A線程就會執行同步方法體,當B線程執行CAS操作的時候就會發現指向的已經不同,Mark Word變為輕量級鎖的狀態,這個時候CAS操作失敗,B線程進入自旋獲取鎖的狀態;
3).B線程獲取自旋鎖失敗,這個時候Mark Word變為重量級鎖,線程阻塞;當A線程執行完成同步方法體,然后在執行CAS操作的時候也是執行失敗,這個時候A線程就進入等待狀態;這個時候大家就回歸重量級鎖的狀態;
5.偏向鎖
偏向鎖主要是為了處理在沒有線程競爭的時候沒必要走向輕量級鎖,為了減少輕量級鎖的CAS操作,接下來看下具體的處理流程:
獲取鎖
1).檢測Mark Word是否為可偏向狀態,如果為否則鎖標記為01;
2).如果為偏向鎖,則檢查線程ID是否為當前ID,如果是則執行同步代碼;
3).如果不是則進行輕量級鎖的流程;
釋放鎖
偏向鎖只有在競爭狀態的情況下才會釋放鎖;
五、結束語
上面文章主要參考深入理解Java虛擬機,如果有不明白的地方可以找我,QQ群:438836709 ;下一篇預告:volatile;