衆所周知, Java併發系列編程一直都是Java程序員難以輕易繞過的山,可謂之小高之山也。Java生態圈中提供了很是豐富的併發編程類庫,可是這樣子也造就了很是多的人知其然而不知其因此然,不少人只會用,殊不知其底層的運行機制,不知其優點與缺陷,也就沒法將其融會貫通,作到信手拈來。況且,即使很是完善的類庫也沒法知足全部的業務需求,適當的時候咱們可能要本身編寫類庫來支撐本身的業務。在這個系列中,咱們一塊兒來深刻學習併發編程,一塊兒成長。本人能力有限,如有錯誤,請及時指正。java
在JDK1.6 之前,
synchronized
關鍵字被咱們認爲是很是重的鎖,相較之Lock,性能讓人聞風喪膽。可是隨着JDK1.6 以後對synchronized
進行了各類優化,好比鎖消除,鎖粗化等等,讓synchronized
的性能基本與Lock相近。本文嘗試剖析底層實現機制,並瞭解JDK1.6版本鎖帶來的各類優化手段,指望可以讓咱們在面對同步問題之時,能夠知其因此然。程序員
在瞭解內部原理以前,咱們先看一段代碼編程
public class SynchronizedTest { public synchronized void synMethod(){ } public void synBlock() { synchronized (this) { } } }
這段代碼是咱們對synchronized
關鍵字用的最多的寫法,也是synchronized
的表現形式,目前咱們最多見的表現形式以下:數組
除開這些表現形式以外,咱們經過命令行的方式輸入javap -v SynchronizedTest.class能夠看到class文件的編譯信息,以下圖,能夠看到同步代碼塊的具體實現:monitorenter
與monitorexit
。monitorenter指令插入到同步代碼塊的開始位置,表示執行臨界區資源時須要獲取monitor鎖。monitorexit指令插入到同步代碼塊的結束位置,表示退出臨界區並釋放鎖資源。JVM須要保證每個monitorenter都有一個monitorexit與之相對應。任何對象都有一個monitor與之相關聯,當且一個monitor被持有以後,將處於鎖定狀態。多線程
synchronized
的實現原理是從獲取monitor
鎖到釋放的過程。具體的執行機制,能夠經過一張圖來展現: 併發
整個執行過程很是清晰,當一個線程嘗試獲取monitor
鎖時,若是monitorenter
成功,則表示當前對象鎖已被該線程持有。若是該線程還未釋放鎖,而其餘線程又嘗試獲取該鎖時,線程會進入同步隊列
中等待已經持有鎖的線程釋放後再繼續與其餘線程競爭。除此以外,這裏還用虛線表示,若是線程已持有該對象鎖,可是發現執行過程當中,還有未知足條件時,那麼當前線程須要讓出該鎖,並等待從新競爭該鎖。流程則是:線程調用wait
時,線程會直接走到monitorexit
,釋放持有的鎖,而後進入等待隊列
中,只有當線程被notify/notifyall
調用時被喚醒,喚醒以後線程會進入同步隊列中,與其餘線程繼續競爭鎖資源。app
文章開頭提到,Java對象自己就是鎖,查看JDK源碼也能夠發現,wait/notify/notifyall
均是Object下的方法,對於整個Java體系而言,Object做爲根類存在,因此Java任何對象也擁有該系列方法,測面也印證了Java對象自己就是鎖。性能
從synchronized的實現原理來看,該關鍵字很是重,因此JDK1.6以後作了很是多的優化,那麼這些優化都有哪些,又是如何優化的呢?學習
在討論鎖優化以前,有必要優先了解一下Java對象頭。測試
HotSpot虛擬機的對象頭主要包括兩部分信息:
MarkWord 數據的長度在32位和64位的虛擬機(未開啓壓縮指針)中分別爲32bit和64bit,它的最後2bit是鎖狀態標誌位,用來標記當前對象的狀態,對象的所處的狀態,決定了markword存儲的內容,以下表所示:
狀態 | 標誌位 | 存儲內容 |
---|---|---|
未鎖定 | 01 | 對象哈希碼、對象分代年齡 |
輕量級鎖定 | 00 | 指向鎖記錄的指針 |
膨脹(重量級鎖定) | 10 | 執行重量級鎖定的指針 |
GC標記 | 11 | 空(不須要記錄信息) |
可偏向 | 01 | 偏向線程ID、偏向時間戳、對象分代年齡 |
TIP: jdk1.6對鎖的實現引入了大量的優化,如自旋鎖、適應性自旋鎖、鎖消除、鎖粗化、偏向鎖、輕量級鎖等技術來減小鎖操做的開銷。 鎖主要存在四中狀態,依次是:無鎖狀態、偏向鎖狀態、輕量級鎖狀態、重量級鎖狀態,他們會隨着競爭的激烈而逐漸升級。注意鎖能夠升級不可降級,這種策略是爲了提升得到鎖和釋放鎖的效率。可是這些優化都不是絕對的優化意義,是否須要這些優化,要根據業務而定,可是瞭解這些優化技術以及優化方式,有助於咱們經過具體業務去決定是否開啓某些優化項。
鎖粗化
原則上,咱們儘量將同步塊的做用範圍限制的儘可能小——只在共享數據操做部分進行同步。可是在某些場景下,一系列的連續操做都對同一個對象進行反覆加鎖與解鎖,甚至加鎖操做時出如今循環體中的,那即時沒有現成競爭,頻繁的進行互斥同步操做也會致使沒必要要的性能損耗,這個時候就須要鎖粗化,將鎖的範圍擴大至整個連續操做的序列。(好比StringBuffer的append操做)
自旋鎖
當多個線程同時訪問共享的數據並能夠在短期內處理完成,這個時候去掛起和恢復線程是不值得。因此自旋鎖的本質就是:當一個線程在等待持有鎖的線程執行完成的時候,自行發起一個忙循環等待當前持有鎖的線程執行完成。在JDK1.6以後自旋鎖設置自動開啓。可是這裏涉及到一個問題就是自旋的時間(自旋的次數),若是線程自旋超過了限定的次數以後,就有必要交給傳統的互斥方式將線程掛起。虛擬機默認是自旋次數爲10,用戶能夠經過使用參數-XX:PreBlockSpin來更改。
自適應鎖
自適應鎖的語義是,自旋鎖的時間由前一次在同一個鎖上的自旋時間,自旋次數以及所的擁有者的狀態來決定。
鎖消除
鎖消除是指虛擬機進行即時編譯運行時,對一些在代碼層面要求同步,可是被檢測到不可能存在共享數據競爭的鎖進行消除。鎖消除的主要斷定依據來源於逃逸分析的數據支持,若是判斷在一段代碼中,堆上的全部數據都不會存在逃逸出去從而被其餘線程訪問到,那就能夠把他們當作棧上數據對待,認爲它們是線程私有的,同步加鎖天然就無須進行。
偏向鎖
偏向鎖在JDK1.6引入,目的是消除數據無競爭狀況下的同步原語,進一步提升程序的性能。偏向鎖在無競爭的狀況下把整個同步原語都消除掉。
當一個線程訪問同步塊並獲取鎖時,會在對象頭和棧幀中的鎖記錄存儲鎖偏向的線程ID,之後該線程在進入和退出同步塊時不須要進行CAS操做來解鎖和解鎖。只須要簡單的測試一下對象的MarkWord
裏是否存儲着指向當前線程的偏向鎖。若是測試成功,表示線程已經得到了鎖。若是測試失敗,則須要在測試一下MarkWord
中偏向鎖的標識是否爲1:若是沒有設置爲1,則使用CAS競爭鎖;若是設置了,則嘗試使用CAS將對象頭的偏向鎖指向當前線程。
偏向鎖使用了一種等到競爭出現才釋放鎖的機制,因此當其餘線程嘗試競爭偏向鎖時,持有偏向鎖的線程纔會釋放鎖。
偏向鎖能夠提升帶有同步但無競爭的程序性能,它是一個帶有權衡效益性質的優化,若是程序大多數鎖都被會被多線程訪問,那偏向鎖模式就是多餘的,-XX:UseBiasedLocking能夠禁止偏向鎖的優化。
輕量級鎖
輕量級鎖是從JDK1.6以後加入的,它的本意是在沒有多線程競爭的前提下,減小傳統的重量級鎖使用操做系統互斥量產生的損耗。當關閉偏向鎖功能或者多個線程競爭偏向鎖致使偏向鎖升級爲輕量級鎖,則會嘗試獲取輕量級鎖。
TIP:輕量級鎖在有競爭的條件下,比重量級鎖更慢。
輕量級鎖加鎖以及膨脹流程,以下圖:
重量級鎖
經過內置鎖Monitor
實現(監視器鎖),Monitor
的本質是依賴於底層操做系統的Mutex Lock
實現,操做系統實現線程之間的切換須要從用戶態到內核態的切換,切換成本很是高。
《深刻了解Java虛擬機》 《Java 併發編程的藝術》 《死磕Java併發》