實現(xiàn)原理
Synchronized可以保證一個在多線程運行中,同一時刻只有一個方法或者代碼塊被執(zhí)行,它還可以保證共享變量的可見性和原子性
在Java中每個對象都可以作為鎖,這是Synchronized實現(xiàn)同步的基礎(chǔ)。具體的表現(xiàn)為一下3種形式:
- 普通同步方法,鎖是當(dāng)前實例對象;
- 靜態(tài)同步方法,鎖是當(dāng)前類的Class對象;
- 同步方法快,鎖是Synchronized括號中配置的對象。
當(dāng)一個線程試圖訪問同步代碼塊時,它必須先獲取到鎖,當(dāng)同步代碼塊執(zhí)行完畢或拋出異常時,必須釋放鎖。那么它是如何實現(xiàn)這一機制的呢?我們先來看一個簡單的synchronized的代碼:
public class SyncDemo {
public synchronized void play() {}
public void learn() {
synchronized(this) {
}
}
}
利用javap工具查看生成的class文件信息分析Synchronized,下面是部分信息
public com.zzw.juc.sync.SyncDemo();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 8: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/zzw/juc/sync/SyncDemo;
public synchronized void play();
descriptor: ()V
flags: ACC_PUBLIC, ACC_SYNCHRONIZED
Code:
stack=0, locals=1, args_size=1
0: return
LineNumberTable:
line 10: 0
LocalVariableTable:
Start Length Slot Name Signature
0 1 0 this Lcom/zzw/juc/sync/SyncDemo;
public void learn();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=3, args_size=1
0: aload_0
1: dup
2: astore_1
3: monitorenter
4: aload_1
5: monitorexit
6: goto 14
9: astore_2
10: aload_1
11: monitorexit
12: aload_2
13: athrow
14: return
Exception table:
from to target type
4 6 9 any
9 12 9 any
從上面利用javap工具生成的信息我們可以看到同步方法是利用ACC_SYNCHRONIZED這個修飾符來實現(xiàn)的,同步代碼塊是利用monitorenter和monitorexit這2個指令來實現(xiàn)的。
- 同步代碼塊:monitorenter指令插入到同步代碼塊的開始位置,monitorexit指令插入到同步代碼塊的結(jié)束位置,JVM需要保證每一個monitorenter都有一個monitorexit與之相對應(yīng)。任何對象都有一個monitor與之相關(guān)聯(lián),當(dāng)且一個monitor被持有之后,他將處于鎖定狀態(tài)。線程執(zhí)行到monitorenter指令時,將會嘗試獲取對象所對應(yīng)的monitor所有權(quán),即嘗試獲取對象的鎖;
- 同步方法:synchronized方法則會被翻譯成普通的方法調(diào)用和返回指令如:invokevirtual、areturn指令,在JVM字節(jié)碼層面并沒有任何特別的指令來實現(xiàn)被synchronized修飾的方法,而是在Class文件的方法表中將該方法的access_flags字段中的synchronized標(biāo)志位置1,表示該方法是同步方法并使用調(diào)用該方法的對象或該方法所屬的Class在JVM的內(nèi)部對象表示Klass做為鎖對象
在繼續(xù)分析Synchronized之前,我們需要理解2個非常重要的概念:Java對象頭和Monitor
Java對象頭
Synchronized用的鎖是存放在Java對象頭里面的。那么什么是對象頭呢?在Hotspot虛擬機中,對象頭包含2個部分:標(biāo)記字段(Mark Word)和類型指針(Kass point)。
其中Klass Point是是對象指向它的類元數(shù)據(jù)的指針,虛擬機通過這個指針來確定這個對象是哪個類的實例,Mark Word用于存儲對象自身的運行時數(shù)據(jù),它是實現(xiàn)輕量級鎖和偏向鎖的關(guān)鍵。這里我們將重點闡述Mark Word。
Mark Word
Mark Word用于存儲對象自身的運行時數(shù)據(jù),如哈希碼(Hash Code)、GC分代年齡、鎖狀態(tài)標(biāo)志、線程持有鎖、偏向線程ID、偏向時間戳等,這部分?jǐn)?shù)據(jù)在32位和64位虛擬機中分別為32bit和64bit。一個對象頭一般用2個機器碼存儲(在32位虛擬機中,一個機器碼為4個字節(jié)即32bit),但如果對象是數(shù)組類型,則虛擬機用3個機器碼來存儲對象頭,因為JVM虛擬機可以通過Java對象的元數(shù)據(jù)信息確定Java對象的大小,但是無法從數(shù)組的元數(shù)據(jù)來確認(rèn)數(shù)組的大小,所以用一塊來記錄數(shù)組長度。
在32位虛擬機中,Java對象頭的Makr Word的默認(rèn)存儲結(jié)構(gòu)如下:
鎖狀態(tài) | 25bit | 4bit | 1bit 是否是偏向鎖 | 2bit鎖標(biāo)志位 |
---|---|---|---|---|
無鎖狀態(tài) | 對象的HashCode | 對象分代年齡 | 0 | 01 |
在程序運行期間,對象頭中鎖表標(biāo)志位會發(fā)生改變。Mark Word可能發(fā)生的變化如下:
在64位虛擬機中,Java對象頭中Mark Work的長度是64位的,其結(jié)構(gòu)如下:
介紹了Mark Word 下面我們來介紹下一個重要的概率Monitor。
Monitor
Monitor是操作系統(tǒng)提出來的一種高級原語,但其具體的實現(xiàn)模式,不同的編程語言都有可能不一樣。Monitor 有一個重要特點那就是,同一個時刻,只有一個線程能進入到Monitor定義的臨界區(qū)中,這使得Monitor能夠達到互斥的效果。但僅僅有互斥的作用是不夠的,無法進入Monitor臨界區(qū)的線程,它們應(yīng)該被阻塞,并且在必要的時候會被喚醒。顯然,monitor 作為一個同步工具,也應(yīng)該提供這樣的機制。Monitor的機制如下圖所示:
從上圖中,我們來分析下Monitor的機制:
Mointor可以看做是一個特殊的房間(這個房間就是我們在Java線程中定義的臨界區(qū)),Monitor在同一時間,保證只能有一個線程進入到這個房間,進入房間即表示持有Monitor,退出房間即表示釋放Monitor。
當(dāng)一個線程需要訪問臨界區(qū)中的數(shù)據(jù)(即需要獲取到對象的Monitro)時,他首先會在entry-set入口隊列中排隊等待(這里并不是真正的按照排隊順序),如果沒有線程持有對象的Monitor,那么entry-set隊列中的線程會和waite-set隊列中被喚醒的線程進行競爭,選出一個線程來持有對象Monitor,執(zhí)行受保護的代碼段,執(zhí)行完畢后釋放Monitor,如果已經(jīng)有線程持有對象的Monitor,那么需要等待其釋放Monitor后再進行競爭。當(dāng)一個線程擁有對象的Monitor后,這個時候如果調(diào)用了Object的wait方法,線程就釋放了Monitor,進入wait-set隊列,當(dāng)Object的notify方法被執(zhí)行后,wait-set中的線程就會被喚醒,然后在wait-set隊列中被喚醒的線程和entry-set隊列中的線程一起通過CPU調(diào)度來競爭對象的Monitor,最終只有一個線程能獲取對象的Monitor。
需要注意的是:
當(dāng)一個線程在wait-set中被喚醒后,并不一定會立刻獲取Monitor,它需要和其他線程去競爭
如果一個線程是從wait-set隊列中喚醒后,獲取到的Monitor,它會去讀取它自己保存的PC計數(shù)器中的地址,從它調(diào)用wait方法的地方開始執(zhí)行。
鎖的優(yōu)化和對比
在JavaSE6為了對鎖進行優(yōu)化,引入了偏向鎖和輕量級鎖。在JavaSE6中鎖一共有4種狀態(tài),它們從低到高一次是無狀態(tài)鎖、偏向鎖、輕量級鎖和重量級鎖。鎖的這幾種狀態(tài)會隨著競爭而依次升級,但是鎖是不能降級的。
偏向鎖
偏向鎖顧名思義就是偏向于第一個訪問鎖的線程,在運行的過程中同步鎖只有一個線程訪問,不存在多線程競爭的情況,則線程不會觸發(fā)同步,這種情況下會給線程加一個偏向鎖。偏向鎖的引入就是為了讓線程獲取鎖的代價更低。
-
偏向鎖的獲取
(1)訪問Mark Word中偏向鎖的標(biāo)識是否設(shè)置成1,鎖標(biāo)志位是否為01——確認(rèn)為可偏向狀態(tài)。
(2)如果為可偏向狀態(tài),則測試線程ID是否指向當(dāng)前線程,如果是,進入步驟(5),否則進入步驟(3)。
(3)如果線程ID并未指向當(dāng)前線程,則通過CAS操作競爭鎖。如果競爭成功,則將Mark Word中線程ID設(shè)置為當(dāng)前線程ID,然后執(zhí)行(5);如果競爭失敗,執(zhí)行(4)。
(4)如果CAS獲取偏向鎖失敗,則表示有競爭。當(dāng)?shù)竭_全局安全點(safepoint)時獲得偏向鎖的線程被掛起,偏向鎖升級為輕量級鎖,然后被阻塞在安全點的線程繼續(xù)往下執(zhí)行同步代碼。
(5)執(zhí)行同步代碼。
-
偏向鎖的釋放
偏向鎖的釋放在上面偏向鎖的獲取中的第4步已經(jīng)提到過。偏向鎖只有在遇到其它線程競爭偏向鎖時,持有偏向鎖的線程才會釋放。線程是不會主動的去釋放偏向鎖的。偏向鎖的釋放需要等到全局安全點(在這個時間點上沒有正在執(zhí)行的字節(jié)碼),它會首先去暫停擁有偏向鎖的線程,撤銷偏向鎖,設(shè)置對象頭中的Mark Word為無鎖狀態(tài)或輕量級鎖狀態(tài),再恢復(fù)暫停的線程。
-
偏向鎖的關(guān)閉
偏向鎖在Java6和Java7中是默認(rèn)開啟的,但它是在應(yīng)用程序啟動幾秒后才激活。如果想消除延時立即開啟,可以調(diào)整JVM參數(shù)來關(guān)閉延遲:-XX: BiasedLockingStartupDelay=0。如果你確定應(yīng)用程序中沒有偏向鎖的存在,你也可以通過JVM參數(shù)關(guān)閉偏向鎖: -XX:UseBiasedLocking=false,使用改參數(shù)后,程序會默認(rèn)進入到輕量級鎖狀態(tài)。
-
偏向鎖的適用場景
始終只有一個線程在執(zhí)行同步塊,在它沒有執(zhí)行完同步代碼塊釋放鎖之前,沒有其它線程去執(zhí)行同步塊來競爭鎖,在鎖無競爭的情況下使用。一旦有了競爭就升級為輕量級鎖,升級為輕量級鎖的時候需要撤銷偏向鎖,撤銷偏向鎖需要在全局安全點上,這個時候會導(dǎo)致Stop The World,Stop The Wrold 會導(dǎo)致性能下降,因此在高并發(fā)的場景下應(yīng)當(dāng)禁用偏向鎖。
輕量級鎖
輕量級鎖是有偏向鎖競爭升級而來的。引入輕量級鎖的目的是在沒有多線程競爭的情況下,減少傳統(tǒng)的重量級鎖使用操作系統(tǒng)互斥量產(chǎn)生的性能消耗。
-
輕量級鎖的獲取
(1)在代碼進入同步代碼塊時,如果同步對象沒有被鎖定(鎖標(biāo)志位為“01”狀態(tài)),虛擬機首先將在當(dāng)前線程的棧幀中建了一個名為鎖記錄(Lock Record)的空間,用于存儲對象目前的Mark Word的拷貝,官方稱之為 Displaced Mark Word。
(2)虛擬機將使用CAS操作嘗試將對象的Mark Word更新為指向Lock Record的指針,如果更新成功,則表示獲取到了鎖,并將鎖標(biāo)志位設(shè)置為“00”(表示對象處于輕量級鎖狀態(tài))。如果失敗則執(zhí)行(3)操作。
(3)虛擬機檢查當(dāng)前對象的Mark Wrod 是否指向當(dāng)前線程的棧幀,如果是這說明當(dāng)前線程已經(jīng)持有了這個對象的鎖,直接進入同步塊繼續(xù)運行;否則說明這個鎖對象已經(jīng)被其它線程持有,這是輕量級鎖就要膨脹為重量級鎖,鎖標(biāo)志的狀態(tài)值變更為“10”,后面等待鎖的線程也要進入阻塞狀態(tài)。
-
輕量級鎖的釋放
(1)使用CAS操作把對象當(dāng)前的Mark Word和線程中復(fù)制的Displaced Mark Word替換回來,如果成功,則同步過程完成。
(2)CAS替換失敗,說明有其他線程嘗試過獲取該鎖,那就要在釋放鎖的同時,喚醒被掛起的線程。
輕量級鎖能提升同步性能的依據(jù)是“對于絕大部分的鎖,在整個同步周期都是不存在競爭的”。若果沒有競爭,輕量級鎖使用CAS操作避免了使用互斥量的開銷,但如果存在鎖競爭,除了互斥量的開銷外,還額外發(fā)成了CAS操作,因此存在競爭的情況下,輕量級鎖比傳統(tǒng)的重量級做會更慢。
重量級鎖
重量級鎖通過對象內(nèi)部的監(jiān)視器(monitor)實現(xiàn),其中monitor的本質(zhì)是依賴于底層操作系統(tǒng)的Mutex Lock實現(xiàn),操作系統(tǒng)實現(xiàn)線程之間的切換需要從用戶態(tài)到內(nèi)核態(tài)的切換,切換成本非常高。
偏向鎖、輕量級鎖的狀態(tài)轉(zhuǎn)換
其它優(yōu)化
-
自旋鎖
線程的掛起和恢復(fù)需要CPU從用戶狀態(tài)切換到核心狀態(tài),頻繁的掛起和恢復(fù)會給系統(tǒng)的并發(fā)性能帶來很大的壓力。同時我們發(fā)現(xiàn)在許多的應(yīng)用上,共享該數(shù)據(jù)的鎖定只會持續(xù)很短的一段時間,為了這一段很短的時間,讓線程頻繁的掛起和恢復(fù)是很不值得的,因此引入了自旋鎖。
自旋鎖的原理非常的簡單,若果那些持有鎖的線程能夠在很短的時間釋放資源,那么那等待競爭鎖的線程就不需要做用戶狀態(tài)和內(nèi)核狀態(tài)的切換進入阻塞掛起狀態(tài),它們只需要“稍等一下”,等待持有鎖的線程釋放資源后立即獲取鎖。這里需要注意的是,線程在自旋的過程中,是不會放棄CPU的執(zhí)行時間的,因此如果鎖被占用的時間很長,那么自旋的線程不做任何有用的工作從而浪費了CPU的資源。所有自旋等待時間必須有一個限制,如果自旋超過了限定的次數(shù)任然沒有獲取鎖,則需要停止自旋進入阻塞狀態(tài)。虛擬機設(shè)定的自旋次數(shù)默認(rèn)是10次,可以通過 -XX:PreBlockSpin來更改。 -
自適自旋鎖
上面說到自旋鎖的自旋次數(shù)是一個固定的值,但是這個自旋次數(shù)應(yīng)該如何限定了,設(shè)置大了會讓線程一直占用CPU時間浪費性能,設(shè)置低了會讓線程頻繁的進入掛起和恢復(fù)狀態(tài)也會浪費性能。因此JDK在1.6中引入了自適應(yīng)自旋鎖,自適應(yīng)說明自旋的時間不在是固定的了,而是由前一次在同一個鎖上的自旋時間以及鎖的擁有者的狀態(tài)來決定的。
自適應(yīng)自旋鎖的原理也非常簡單,當(dāng)一個線程在一把鎖上自旋成功,那么下一次在這個鎖上自旋的時間將更長,因為虛擬機認(rèn)為上次自旋成功了,那么這次自旋也有可能再次成功。反之,如果一個線程在一個鎖上很少自旋成功,那么以后這個線程要獲取這個鎖時,自旋的此時將會減少甚至可能省略自旋的過程,直接進入阻塞狀態(tài)以免浪費CPU的資源。 -
鎖消除
鎖消除是指虛擬機即時編譯器在運行時,對一些代碼上要求同步,但是被檢測到不可能存在共享數(shù)據(jù)競爭的鎖進行消除。鎖消除的主要判定依據(jù)是逃逸分析的數(shù)據(jù)支持。變量是否逃逸對于虛擬機來說需要使用數(shù)據(jù)流來分析,但是對于我們程序員應(yīng)該是很清楚的,怎么會在知道不存在數(shù)據(jù)競爭的情況下使用同步呢?但是程序有時并不是我們想的那樣,雖然我們沒有顯示的使用鎖,但是在使用一些Java 的API時,會存在隱式加鎖的情況。例如如下代碼:
public String concat(String s1, String s2){
StringBuffer sb = new StringBuffer();
sb.append(s1);
sb.append(s2);
return sb.toString();
}
我們知道每個sb.append()方法中都有一個同步快,鎖就是sb的對象。因此虛擬機在運行這段代碼時,會監(jiān)測到sb這個變量永遠不會“逃逸”到concat()方法之外,因此虛擬機就會消除這段代碼中的鎖而直接執(zhí)行了。
-
鎖粗化
我們知道在使用同步鎖的時候,需要盡量將同步塊的作用范圍限制的盡量小一些----只在共享數(shù)據(jù)的實際作用域中才進行同步,這樣做的目的是為了是同步的時間盡可能的縮短,如果存在鎖的競爭,那么等待鎖的線程也能盡快的獲取到鎖。
大多數(shù)情況下,上面的的原則都是正確的。但是如果一系列的連續(xù)操作都對同一個對象反復(fù)的加鎖,甚至加鎖出現(xiàn)在循環(huán)體中,那么即時沒有競爭,頻繁的進行互斥同步操作也會導(dǎo)致不必須的性能損耗。所以引入了鎖粗化的概率。
那么什么是鎖粗化呢?鎖粗化就是將連接加鎖、解鎖的過程連接在一起,擴展(粗化)成為一個同步范圍更大的鎖。以上面代碼為例,就是擴展到第一個append()操作之前,直至最后一個append()操作之后,這樣只需要加鎖一次就可以了。
總結(jié)
本文重點探究了Synchronized的實現(xiàn)原理,以及JDK引入偏向鎖和輕量級鎖對synchronized所做的優(yōu)化處理,和一些其他的鎖的優(yōu)化處理。我們最后來總結(jié)一下Synchronized的執(zhí)行過程:
- 檢測Mark Word里面是不是當(dāng)前線程的ID,如果是,表示當(dāng)前線程處于偏向鎖 。
- 如果不是,則使用CAS將當(dāng)前線程的ID替換Mard Word,如果成功則表示當(dāng)前線程獲得偏向鎖,置偏向標(biāo)志位1 。
- 如果失敗,則說明發(fā)生競爭,撤銷偏向鎖,進而升級為輕量級鎖。
- 當(dāng)前線程使用CAS將對象頭的Mark Word替換為鎖記錄指針,如果成功,當(dāng)前線程獲得鎖 。
- 如果失敗,表示其他線程競爭鎖,當(dāng)前線程便嘗試使用自旋來獲取鎖。
- 如果自旋成功則依然處于輕量級狀態(tài)。
- 如果自旋失敗,則升級為重量級鎖。