1 JDK1.6對synchronized關鍵字優化概覽
2 synchronized鎖更新過程
2.1 偏向鎖( Biased Locking ) --- 适用于同一個線程反複進入同步代碼塊的情況
2.1.1 什麼是偏向鎖
2.1.2 偏向鎖加鎖 + 撤銷原理
2.1.3 偏向鎖的驗證
2.1.4 偏向鎖的好處
小結
2.2 輕量級鎖 (Lightweight Locking)--- 适用于線程交替進入同步方法的情況
2.2.1 什麼是輕量級鎖
2.2.2 輕量級鎖加鎖 + 撤銷原理
2.2.3 輕量級鎖的驗證
2.2.4 輕量級鎖好處
2.3 自旋
2.3.1 自旋鎖
2.3.2 适應性自旋鎖
2.3.3 自旋在JDK(hotspot)中的源碼
3 JDK1.6對synchronized關鍵字的其他優化簡介
3.1 鎖消除
3.2 鎖粗化
源碼位址:https://github.com/nieandsun/concurrent-study.git
前面的文章《【并發程式設計】 — 從位元組碼指令的角度去了解synchronized關鍵字的原理》、《【并發程式設計】 — 從JVM源碼的角度進一步去了解synchronized關鍵字的原理》已經介紹過在JDK1.6之前synchronized關鍵字的處理邏輯是<code>隻要線程想進入</code>同步代碼塊<code>就會</code>先去<code>調用核心函數</code>去搶占鎖對象關聯的monitor的所有權。
調用核心函數就會涉及到核心态和使用者态的切換問題,這會消耗大量的系統資源,降低程式運作效率。而有研究表明大多數情況下代碼都是交替進行執行的,<code>交替執行就不會産生并發,自然也就不會帶來并發安全問題,</code>是以JDK1.6之前synchronized關鍵字的這種機制是有一定問題的 。
Doug Lea搞得Reentrantlock其實就很好的解決了該問題,可以看一下我這篇文章《【并發程式設計】 — Reentrantlock源碼解析1:同步方法交替執行的處理邏輯》 。
應該是因為synchronized關鍵字是JDK原有關鍵字的原因吧,HotSpot虛拟機開發團隊在JDK1.6這個版本上花費了大量的精力去實作各種鎖優化技術,包括偏向鎖( Biased Locking )、輕量級鎖( Lightweight Locking )、适應性自旋(Adaptive Spinning)、鎖消除( Lock Elimination)、鎖粗化( Lock Coarsening )等 —》這些技術都是為了線上程之間更高效地共享資料,以及解決競争問題,進而提高程式的執行效率。
鎖的更新過程為: 無鎖 —> 偏向鎖 —> 輕量級鎖 —> 重量級鎖
偏向鎖是JDK 6中的重要引進,因為HotSpot作者經過研究實踐發現,在<code>大多數情況下</code>,鎖不僅不存在多線程競争,而且總是由同一線程多次獲得,為了讓線程獲得鎖的代價更低,引進了偏向鎖。
偏向鎖的“偏”,就是偏心的“偏”、偏袒的“偏”,它的意思是這個鎖會偏向于第一個獲得它的線程,會在對象頭存儲鎖偏向的線程ID,以後該線程進入和退出同步塊時隻需要檢查是否為偏向鎖、鎖标志位以及ThreadID即可。
不過一旦出現多個線程競争時必須撤銷偏向鎖,是以撤銷偏向鎖消耗的性能必須小于之前節省下來的CAS原子操作的性能消耗,不然就得不償失了。
【加鎖】
當線程第一次通路同步塊并擷取鎖時,偏向鎖處理流程如下:
(1)虛拟機将會把對象頭中的标志位設為“01”,即偏向模式。 (2)同時使用CAS操作把擷取到這個鎖的線程的ID記錄在對象的Mark Word之中 ,如果CAS操作成功,持有偏向鎖的線程以後每次進入這個鎖相關的同步塊時,虛拟機都可以不再進行任何同步操作,偏向鎖的效率高。
結合Mark Word(可以參看我上篇部落格《【并發程式設計】 — 原來java對象的布局是可以被這樣證明的!!!》)存儲結構可以更好的進行了解:

【撤銷】
偏向鎖的撤銷過程如下:
(1)偏向鎖的撤銷動作必須等待全局安全點(這是JVM決定的,一般将循環的末尾、方法傳回前等作為安全點) (2)挂起擁有偏向鎖的線程,判斷鎖對象是否處于被鎖定狀态 (3)撤銷偏向鎖,恢複到無鎖(标志位為 01 )或輕量級鎖(标志位為 00)的狀态
【加鎖+撤銷過程原理圖】
偏向鎖加鎖 + 撤銷原理可以用下圖進行表示:
這是《Java并發程式設計的藝術》中對該過程的解釋圖,畫的挺好的,這裡拿來用一下☺☺☺。
偏向鎖在Java 6之後是預設啟用的,但在應用程式啟動幾秒鐘之後才激活,可以使用 <code>-XX:BiasedLockingStartupDelay=0</code>參數關閉延遲,如果确定應用程式中所有鎖通常情況下處于競争狀态,可以通過<code>XX:-UseBiasedLocking=false</code> 參數關閉偏向鎖。
驗證程式如下:
要想獲得想要的效果,在運作時,需加上如下VM參數`
運作結果如下:
綠色部分為Mark Word的前56位存放了線程的ThreadId 和Epoch ,可以看到這56位的值都一樣 黃色部分為Mark Word的最後三位101
這個結果與2.1.2中表裡描述的一緻。
偏向鎖是在隻有一個線程執行同步塊時進一步提高性能,适用于一個線程反複獲得同一鎖的情況。偏向鎖可以提高帶有同步但無競争的程式性能。
它同樣是一個帶有效益權衡性質的優化,也就是說,它并不一定總是對程式運作有利,如果程式中大多數的鎖總是被多個不同的線程通路比如線程池,那偏向模式就是多餘的。
在JDK5中偏向鎖預設是關閉的,而到了JDK6中偏向鎖已經預設開啟。但在應用程式啟動幾秒鐘之後才激活,可以使用 <code>-XX:BiasedLockingStartupDelay=0</code>參數關閉延遲,如果确定應用程式中所有鎖通常情況下處于競争狀态,可以通過 <code>XX:-UseBiasedLocking=false</code> 參數關閉偏向鎖。
偏向鎖的原理:
當鎖對象第一次被線程擷取的時候, 虛拟機将會把其對象頭中的标志位設為“101”, 即偏向模式。 同時使用CAS操作把擷取到這個鎖的線程的ID記錄在鎖對象的Mark Word之中 , 如果CAS操作成功, 持有偏向鎖的線程以後每次進入這個鎖相關的同步塊時, 虛拟機都可以不再進行任何同步操作, 偏向鎖的效率高。
偏向鎖的好處
偏向鎖是在隻有一個線程執行同步塊時進一步提高性能, 适用于一個線程反複獲得同一鎖的情況。 偏向鎖可以提高帶有同步但無競争的程式性能。
輕量級鎖是JDK 6之中加入的新型鎖機制,它名字中的“輕量級”是相對于使用monitor的傳統鎖而言的,是以傳統的鎖機制就稱為“重量級”鎖。首先需要強調一點的是:輕量級鎖并不是用來代替重量級鎖的。
引入輕量級鎖的目的:在多線程交替執行同步塊的情況下,盡量避免重量級鎖引起的性能消耗,但是如果多個線程在同一時刻進入臨界區,會導緻輕量級鎖膨脹更新為重量級鎖,是以輕量級鎖的出現并非是要替代重量級鎖。
當關閉偏向鎖功能或者多個線程競争偏向鎖導緻偏向鎖更新為輕量級鎖,則會嘗試擷取輕量級鎖,其步驟如下:
(1)判斷目前對象是否處于無鎖狀态( hashcode、 0、 01 ),如果是,則JVM首先将在目前線程的棧幀中建立一個名為鎖記錄( Lock Record)的空間,用于存儲鎖對象目前的Mark Word的拷貝(官方把這份拷貝加了一個Displaced字首,即Displaced Mark Word),将對象的Mark Word複制到棧幀中的Lock Record中,将Lock Reocrd中的owner指向目前對象。 (2)JVM利用CAS操作嘗試将對象的Mark Word更新為指向Lock Record的指針,如果成功表示競争到鎖,則将鎖标志位變成00,執行同步操作。 (3)如果失敗則判斷目前對象的Mark Word是否指向目前線程的棧幀,如果是則表示目前線程已經持有目前對象的鎖,則直接執行同步代碼塊;否則隻能說明該鎖對象已經被其他線程搶占了,這時輕量級鎖需要膨脹為重量級鎖,鎖标志位變成10,後面等待的線程将會進入阻塞狀态。
輕量級鎖的釋放也是通過CAS操作來進行的,主要步驟如下:
(1)取出在擷取輕量級鎖儲存在Displaced Mark Word中的資料。 (2)用CAS操作将取出的資料替換目前對象的Mark Word中,如果成功,則說明釋放鎖成功。 (3)如果CAS操作替換失敗,說明有其他線程嘗試擷取該鎖,則需要将輕量級鎖膨脹更新為重量級鎖。
這裡依舊借用《Java并發程式設計的藝術》中的圖:☺☺☺。
感興趣的自己試試吧,可能會獲得意想不到的收獲。。。
對于輕量級鎖,其性能提升的依據是<code>“對于絕大部分的鎖,在整個生命周期内都是不會存在競争的”</code>,如果打破這個依據則除了互斥的開銷外,還有額外的CAS操作,是以在有多線程競争的情況下,輕量級鎖比重量級鎖更慢。
在多線程交替執行同步塊的情況下,可以避免重量級鎖引起的性能消耗。
相信通過前面幾篇文章的鋪墊大家應該已經知道,使用monitor鎖會調用核心函數對線程進行park 和unpark,即線程的park和unpark需要CPU進行使用者态轉和核心态的來回切換。頻繁的park和unpark對CPU來說是一件負擔很重的工作,這些操作會給系統的并發性能帶來很大的壓力。
同時,虛拟機的開發團隊也注意到在許多應用上,共享資料的鎖定狀态<code>隻會持續很短的一段時間</code>,為了這段時間阻塞和喚醒線程并不值得。如果實體機器有一個以上的處理器,能讓兩個或以上的線程同時并行執行,我們就可以讓後面請求鎖的那個線程<code>“稍等一下”,</code>但不放棄處理器的執行時間,看看持有鎖的線程是否很快就會釋放鎖。為了讓線程等待,我們可以<code>讓線程執行一個死循環(自旋)</code> , 這項技術就是所謂的自旋鎖。—》 自旋鎖在JDK 1.4.2中就已經引入 ,隻不過預設是關閉的,可以使用-XX:+UseSpinning參數來開啟,在JDK 1.6中就已經改為預設開啟了。
<code>自旋等待不能代替阻塞</code>,且先不說對處理器數量的要求,自旋等待本身雖然避免了線程切換的開銷,但它是要占用處理器時間的,是以,如果鎖被占用的時間很短,自旋等待的效果就會非常好;反之,如果鎖被占用的時間很長。那麼自旋的線程隻會白白消耗處理器資源,而不會做任何有用的工作,反而會帶來性能上的浪費。是以,自旋等待的時間必須要有一定的限度,如果自旋超過了限定的次數仍然沒有成功獲得鎖,就應當使用傳統的方式去挂起線程了。<code>自旋次數的預設值是10次,</code>使用者可以使用參數-XX : PreBlockSpin來更改 —》 但是這個<code>自旋次數是很難判斷</code>的,是以jdk1.6引入了适應性自旋鎖。
在JDK1.6中還引入了自适應的自旋鎖。自适應意味着自旋的<code>時間不再固定了</code>,而是由前一次在同一個鎖上的自旋時間及鎖的擁有者的狀态來決定。
如果在同一個鎖對象上,自旋等待剛剛成功獲得過鎖,并且持有鎖的線程正在運作中,那麼虛拟機就會認為這次自旋也很有可能再次成功,進而它将允許自旋等待持續相對更長的時間,比如100次循環。
另外,如果對于某個鎖,自旋很少成功獲得過,那在以後要擷取這個鎖時将可能省略掉自旋過程,以避免浪費處理器資源。
有了自适應自旋,随着程式運作和性能監控資訊的不斷完善,虛拟機對程式鎖的狀況預測就會越來越準确,虛拟機就會變得越來越“聰明”了。
源碼所處檔案<code>src/share/vm/runtime/objectMonitor.cpp</code>
自旋次數定義的源碼:
鎖消除是指虛拟機即時編譯器( JIT)在運作時,對一些代碼上要求同步,但是被檢測到不可能存在共享資料競争的鎖進行消除。鎖消除的主要判定依據來源于<code>逃逸分析</code>的資料支援,如果判斷在一段代碼中,堆上的所有資料都不會逃逸出去進而被其他線程通路到,那就可以把它們當做棧上資料對待,認為它們是線程私有的,同步加鎖自然就無須進行。變量是否逃逸,對于虛拟機來說需要使用資料流分析來确定,但是程式員自己應該是很清楚的,怎麼會在明知道不存在資料争用的情況下還要要求同步呢?實際上有許多同步措施并不是程式員自己加入的,同步的代碼在Java程式中的普遍程度也許超過了大部分讀者的想象。
比如,下面這段非常簡單的代碼僅僅是輸出3個字元串相加的結果,無論是源碼字面上還是程式語義上都沒有同步。
StringBuffer的append ( ) 是一個同步方法,鎖就是this也就是(new StringBuilder())。虛拟機發現它的動态作用域被限制在concatString( )方法内部。也就是說, new StringBuilder()對象的引用永遠不會“逃逸”到concatString ( )方法之外,其他線程無法通路到它,是以,雖然這裡有鎖,但是可以被安全地消除掉,在即時編譯之後,這段代碼就會忽略掉所有的同步而直接執行了。
原則上,我們在編寫代碼的時候,總是推薦将同步塊的作用範圍限制得盡量小,隻在共享資料的實際作用域中才進行同步,這樣是為了使得需要同步的操作數量盡可能變小,如果存在鎖競争,那等待鎖的線程也能盡快拿到鎖。
大部分情況下,上面的原則都是正确的,但是如果一系列的連續操作都對同一個對象反複加鎖和解鎖,甚至加鎖操作是出現在循環體中的,那即使沒有線程競争,頻繁地進行互斥同步操作也會導緻不必要的性能損耗。比如如下代碼:
那什麼是鎖粗化,相信你肯定就明白了,給個定義如下:
JVM會探測到一連串細小的操作都使用同一個對象加鎖,将同步代碼塊的範圍放大,放到這串操作的外面,這樣隻需要加一次鎖即可。
end