說到鎖,都會提 synchronized 。這個英文單詞兒啥意思呢?翻譯成中文就是「同步」的意思java
通常都是使用 synchronized 這個關鍵字來給一段代碼或者一個方法上鎖,使得這段代碼或者方法,在同一個時刻只能有一個線程來執行它。web
synchronized 相比於 volatile 來講,用的比較靈活,你能夠在方法上使用,能夠在靜態方法上使用,也能夠在代碼塊上使用。算法
關於 synchronized 這一塊大概就說到這裏,阿粉今天想着重來講一下, synchronized 底層是怎麼實現的編程
我知道能夠利用 synchronized 關鍵字來給程序進行加鎖,可是它具體怎麼實現的我不清楚呀,別急,我們先來看個 demo :數組
public class demo { public void synchronizedDemo(Object lock{ synchronized(lock){ lock.hashCode(); } } }
上面是我寫的一個 demo ,而後進入到 class 文件所在的目錄下,使用 javap -v demo.class
來看一下編譯的字節碼(在這裏我截取了一部分):安全
public void synchronizedDemo(java.lang.Object); descriptor: (Ljava/lang/Object;)V flags: ACC_PUBLIC Code: stack=2, locals=4, args_size=2 0: aload_1 1: dup 2: astore_2 3: monitorenter 4: aload_1 5: invokevirtual #2 // Method java/lang/Object.hashCode:()I 8: pop 9: aload_2 10: monitorexit 11: goto 19 14: astore_3 15: aload_2 16: monitorexit 17: aload_3 18: athrow 19: return Exception table: from to target type 4 11 14 any 14 17 14 any
應該可以看到當程序聲明 synchronized 代碼塊時,編譯成的字節碼會包含 monitorenter
和 monitorexit
指令,這兩種指令會消耗操做數棧上的一個引用類型的元素(也就是 synchronized 關鍵字括號裏面的引用),做爲所要加鎖解鎖的鎖對象。若是看的比較仔細的話,上面有一個 monitorenter
指令和兩個 monitorexit
指令,這是 Java 虛擬機爲了確保得到的鎖不論是在正常執行路徑,仍是在異常執行路徑上都可以解鎖。多線程
關於 monitorenter
和 monitorexit
,能夠理解爲每一個鎖對象擁有一個鎖計數器和一個指向持有該鎖的線程指針:併發
爲何採用這種方式呢?是爲了容許同一個線程重複獲取同一把鎖。好比,一個 Java 類中擁有好多個 synchronized 方法,那這些方法之間的相互調用,不論是直接的仍是間接的,都會涉及到對同一把鎖的重複加鎖操做。這樣去設計的話,就能夠避免這種狀況。函數
在 Java 多線程中,全部的鎖都是基於對象的。也就是說, Java 中的每個對象均可以做爲一個鎖。你可能會有疑惑,不對呀,不是還有類鎖嘛。可是 class 對象也是特殊的 Java 對象,因此呢,在 Java 中全部的鎖都是基於對象的性能
在 Java6 以前,全部的鎖都是"重量級"鎖,重量級鎖會帶來一個問題,就是若是程序頻繁得到鎖釋放鎖,就會致使性能的極大消耗。爲了優化這個問題,引入了"偏向鎖"和"輕量級鎖"的概念。因此在 Java6 及其之後的版本,一個對象有 4 種鎖狀態:無鎖狀態,偏向鎖狀態,輕量級鎖狀態,重量級鎖狀態。
在 4 種鎖狀態中,無鎖狀態應該比較好理解,無鎖就是沒有鎖,任何線程均可以嘗試修改,因此這裏就一筆帶過了。
隨着競爭狀況的出現,鎖的升級很是容易發生,可是若是想要讓鎖降級,條件很是苛刻,有種你想來能夠,可是想走不行的趕腳。
阿粉在這裏囉嗦一句:不少文章說,鎖若是升級以後是不能降級的,其實在 HotSpot JVM 中,是支持鎖降級的鎖降級發生在 Stop The World 期間,當 JVM 進入安全點的時候,會檢查有沒有閒置的鎖,若是有就會嘗試進行降級
看到 Stop The World 和 安全點 可能有人比較懵,我這裏簡單說一下,具體還須要讀者本身去探索一番.(由於這是 JVM 的內容,這篇文章的重點不是 JVM )
在 Java 虛擬機裏面,傳統的垃圾回收算法採用的是一種簡單粗暴的方式,就是 Stop-the-world ,而這個 Stop-the-world 就是經過安全點( safepoint )機制來實現的,安全點是什麼意思呢?就是 Java 程序在執行本地代碼時,若是這段代碼不訪問 Java 對象/調用 Java 方法/返回到原來的 Java 方法,那 Java 虛擬機的堆棧就不會發生改變,這就表明執行的這段本地代碼能夠做爲一個安全點。當 Java 虛擬機收到 Stop-the-world 請求時,它會等全部的線程都到達安全點以後,才容許請求 Stop-the-world 的線程進行獨佔工做
接下來就介紹一下幾種鎖和鎖升級
在剛開始就說了, Java 的鎖都是基於對象的,那是怎麼告訴程序我是個鎖呢?就不得不來講, Java 對象頭 每一個 Java 對象都有對象頭,若是是非數組類型,就用 2 個字寬來存儲對象頭,若是是數組,就用 3 個字寬來存儲對象頭。在 32 位處理器中,一個字寬是 32 位;在 64 位處理器中,字寬就是 64 位咯~對象頭的內容就是下面這樣:
我們主要來看 Mark Word 的內容:
從上面表格中,應該可以看到,是偏向鎖時, Mark Word
存儲的是偏向鎖的線程 ID ;是輕量級鎖時, Mark Word
存儲的是指向線程棧中 Lock Record
的指針;是重量級鎖時, Mark Word
存儲的是指向堆中的 monitor
對象的指針
HotSpot 的做者通過大量的研究發現,在大多數狀況下,鎖不只不存在多線程競爭,並且老是由同一線程屢次得到
基於此,就引入了偏向鎖的概念
因此啥是偏向鎖呢?用大白話說就是,我如今給鎖設置一個變量,當一個線程請求的時候,發現這個鎖是 true
,也就是說這個時候沒有所謂的資源競爭,那也不用走什麼加鎖/解鎖的流程了,直接拿來用就行。可是若是這個鎖是 false
的話,說明存在其餘線程競爭資源,那我們再走正規的流程
看一下具體的實現原理:
當一個線程第一次進入同步塊時,會在對象頭和棧幀中的鎖記錄中存儲鎖偏向的線程 ID 。當下次該線程進入這個同步塊時,會檢查鎖的 Mark Word 裏面存放的是否是本身的線程 ID。若是是,說明線程已經得到了鎖,那麼這個線程在進入和退出同步塊時,都不須要花費 CAS 操做來加鎖和解鎖;若是不是,說明有另一個線程來競爭這個偏向鎖,這時就會嘗試使用 CAS 來替換 Mark Word 裏面的線程 ID 爲新線程的 ID 。此時會有兩種狀況:
偏向鎖使用了一種等到競爭出現時才釋放鎖的機制。也就說,若是沒有人來和我競爭鎖的時候,那麼這個鎖就是我獨有的,當其餘線程嘗試和我競爭偏向鎖時,我會釋放這個鎖
在偏向鎖向輕量級鎖升級時,首先會暫停擁有偏向鎖的線程,重置偏向鎖標識,看起來這個過程挺簡單的,可是開銷是很大的,由於:
你覺得就是升級一個輕量級鎖?too young too simple
偏向鎖向輕量級鎖升級的過程當中,是很是耗費資源的,若是應用程序中全部的鎖一般都處於競爭狀態,偏向鎖此時就是一個累贅,此時就能夠經過 JVM 參數關閉偏向鎖: -XX:-UseBiasedLocking=false
,那麼程序默認會進入輕量級鎖狀態
最後,來張圖吧~
若是多個線程在不一樣時段獲取同一把鎖,也就是不存在鎖競爭的狀況,那麼 JVM 就會使用輕量級鎖來避免線程的阻塞與喚醒
JVM 會爲每一個線程在當前線程的棧幀中建立用於存儲鎖記錄的空間,稱之爲 Displaced Mark Word 。若是一個線程得到鎖的時候發現是輕量級鎖,就會將鎖的 Mark Word 複製到本身的 Displaced Mark Word 中。以後線程會嘗試用 CAS 將鎖的 Mark Word 替換爲指向鎖記錄的指針。
若是替換成功,當前線程得到鎖,那麼整個狀態仍是 輕量級鎖
狀態
若是替換失敗了呢?說明 Mark Word 被替換成了其餘線程的鎖記錄,那就嘗試使用自旋來獲取鎖.(自旋是說,線程不斷地去嘗試獲取鎖,通常都是用循環來實現的)
自旋是耗費 CPU 的,若是一直獲取不到鎖,線程就會一直自旋, CPU 那麼寶貴的資源就這麼被白白浪費了
解決這個問題最簡單的辦法就是指定自旋的次數,好比若是沒有替換成功,那就循環 10 次,尚未獲取到,那就進入阻塞狀態
可是 JDK 採用了一個更加巧妙的方法---適應性自旋。就是說,若是此次線程自旋成功了,那我下次自旋次數更多一些,由於我此次自旋成功,說明我成功的機率仍是挺大的,下次自旋次數就更多一些,那麼若是自旋失敗了,下次我自旋次數就減小一些,就好比,已經看到了失敗的前兆,那我就先溜,而不是非要「不撞南牆不回頭」
自旋失敗以後,線程就會阻塞,同時鎖會升級成重量級鎖
在釋放鎖時,當前線程會使用 CAS 操做將 Displaced Mark Word 中的內容複製到鎖的 Mark Word 裏面。若是沒有發生競爭,這個複製的操做就會成功;若是有其餘線程由於自旋屢次致使輕量級鎖升級成了重量級鎖, CAS 操做就會失敗,此時會釋放鎖同時喚醒被阻塞的過程
一樣,來一張圖吧:
重量級鎖依賴於操做系統的互斥量( mutex )來實現。可是操做系統中線程間狀態的轉換須要相對比較長的時間(由於操做系統須要從用戶態切換到內核態,這個切換成本很高),因此重量級鎖效率很低,可是有一點就是,被阻塞的線程是不會消耗 CPU 的
每個對象均可以當作一個鎖,那麼當多個線程同時請求某個對象鎖時,它會怎麼處理呢?
對象鎖會設置集中狀態來區分請求的線程:
Contention List:全部請求鎖的線程將被首先放置到該競爭隊列Entry List: Contention List 中那些有資格成爲候選人的線程被移到 Entry List 中
Wait Set:調用 wait 方法被阻塞的線程會被放置到 Wait Set 中
OnDeck:任什麼時候刻最多隻能有一個線程正在競爭鎖,該線程稱爲 OnDeck
Owner:得到鎖的線程稱爲 Owner
!Owner:釋放鎖的線程
當一個線程嘗試得到鎖時,若是這個鎖被佔用,就會把該線程封裝成一個 ObjectWaiter
對象插入到 Contention List 隊列的隊首,而後調用 park
函數掛起當前線程
當線程釋放鎖時,會從 Contention List 或者 Entry List 中挑選一個線程進行喚醒
若是線程在得到鎖以後,調用了 Object.wait
方法,就會將該線程放入到 WaitSet 中,當被 Object.notify
喚醒後,會將線程從 WaitSet 移動到 Contention List 或者 Entry List 中。
可是,當調用一個鎖對象的 wait
或 notify
方法時,若是當前鎖的狀態是偏向鎖或輕量級鎖,則會先膨脹成重量級鎖
synchronized 關鍵字是經過 monitorenter 和 monitorexit 兩種指令來保證鎖的
當一個線程準備獲取共享資源時:
輕量級鎖
的狀態重量級鎖
的狀態,此時,自旋的線程進行阻塞,等待以前線程執行完成而且喚醒本身參考:
到這裏,整篇文章的內容就算是結束了。