1 synchronized的三種應(yīng)用方式
- 修飾實例方法: 作用于當(dāng)前對象實例this加鎖解藻,進入同步代碼前要獲得當(dāng)前對象實例的鎖。
- 修飾靜態(tài)方法: 也就是給當(dāng)前類加鎖葡盗,會作用于類的所有對象實例螟左。如果一個線程 A 調(diào)用一個實例對象的非靜態(tài) synchronized 方法,而線程 B 需要調(diào)用這個實例對象所屬類的靜態(tài) synchronized 方法觅够,是允許的胶背,不會發(fā)生互斥現(xiàn)象,因為訪問靜態(tài) synchronized 方法占用的鎖是當(dāng)前類的鎖喘先,而訪問非靜態(tài) synchronized 方法占用的鎖是當(dāng)前實例對象鎖奄妨。簡單來說,就是獲取了類鎖的線程和獲取了對象鎖的線程是不沖突的苹祟!
- 修飾代碼塊: 指定加鎖對象砸抛,對給定對象加鎖,進入同步代碼庫前要獲得給定對象的鎖树枫。
2 synchronized底層實現(xiàn)原理
理解synchronized底層實現(xiàn)原理之前直焙,首先要理解一下Java對象頭于Monitor監(jiān)視器鎖對象。
關(guān)于Java對象頭和Monitor監(jiān)視器對象的介紹見底部補充內(nèi)容
有了上面的基礎(chǔ)砂轻,這里主要從三個方面展開對synchronized底層實現(xiàn)原理進行分析
- Java字節(jié)碼層面
- JVM層面奔誓,隨著多線程的競爭情況,會進行鎖升級的過程
- 在更底層的匯編實現(xiàn)搔涝,使用了
lock comxchg
實現(xiàn)厨喂。
2.1 從Java字節(jié)碼出發(fā)
synchronized關(guān)鍵字根據(jù)不同的使用方法,會有不同的字節(jié)碼實現(xiàn)庄呈,但是最終得原理都是一樣的蜕煌,都是獲取Monitor對象實現(xiàn)同步。
-
synchronized 對同步代碼塊進行加鎖分析
public class SynchronizedDemo { public void method() { synchronized (this) { System.out.println("synchronized 代碼塊"); } } }
javac
編譯成字節(jié)碼文件之后
img
可以看到synchronized 同步語句塊的實現(xiàn)使用的是monitorenter
和monitorexit
指令诬留,其中monitorenter
指令指向同步代碼塊的開始位置斜纪,monitorexit
指令則指明同步代碼塊的結(jié)束位置贫母。 synchronized 對方法進行同步加鎖分析
public class SynchronizedDemo2 {
public synchronized void method() {
System.out.println("synchronized 方法");
}
}
? 編譯后的字節(jié)碼文件為:
? 可以看到修飾同步方法的時候,使用到了ACC_SYNCHRONIZED
標識盒刚,區(qū)別與同步代碼塊腺劣,JVM 通過該ACC_SYNCHRONIZED
訪問標志來辨別一個方法是否聲明為同步方法,從而執(zhí)行相應(yīng)的同步調(diào)用因块。底層都是獲取Monitor監(jiān)視器對象橘原。
2.2 JVM實現(xiàn)鎖升級的過程
JDK1.6
對鎖的實現(xiàn)引入了大量的優(yōu)化,如偏向鎖涡上、輕量級鎖靠柑、自旋鎖、適應(yīng)性自旋鎖吓懈、鎖消除歼冰、鎖粗化等技術(shù)來減少鎖操作的開銷。
鎖主要存在四種狀態(tài)耻警,依次是:無鎖狀態(tài)隔嫡、偏向鎖狀態(tài)、輕量級鎖狀態(tài)甘穿、重量級鎖狀態(tài)腮恩,他們會隨著競爭的激烈而逐漸升級。
鎖升級的詳細過程如下:
3 Java虛擬機對synchronized的優(yōu)化
synchronized在修飾方法和代碼塊在字節(jié)碼上實現(xiàn)方式有很大差異温兼,但是內(nèi)部實現(xiàn)還是基于對象頭的MarkWord
來實現(xiàn)的秸滴。JDK1.6
之前synchronized使用的是重量級鎖,JDK1.6
之后進行了優(yōu)化募判,擁有了無鎖->偏向鎖->輕量級鎖->重量級鎖的升級過程荡含,而不是無論什么情況都使用重量級鎖。
[圖片上傳失敗...(image-784773-1591625443207)]
首先大致了解一下每種鎖存在的意義届垫、
無鎖:
MarkWord
標志位01释液,沒有線程執(zhí)行同步方法/代碼塊時的狀態(tài)。-
偏向鎖:在鎖不存在競爭的時候装处,大部分情況下误债,兩次獲取到鎖的線程都是同一個線程,那么這種情況下實際上實現(xiàn)完整的加鎖過程實際上是特別消耗資源的妄迁,這個時候用的就是偏向鎖寝蹈,可以簡單理解它只是一個標記,沒有任何功能登淘。
偏向鎖是通過在
bitfields
中通過CAS設(shè)置當(dāng)前正在執(zhí)行的ThreadID
來實現(xiàn)的箫老。假設(shè)線程A獲取偏向鎖執(zhí)行代碼塊(即對象頭設(shè)置了ThreadA_ID
),線程A同步塊未執(zhí)行結(jié)束時形帮,線程B通過CAS嘗試設(shè)置ThreadB_ID
會失敗槽惫,因為存在鎖競爭情況,這時候就需要升級為輕量級鎖辩撑。(通過CAS操作設(shè)置線程ID) -
輕量級鎖:當(dāng)鎖存在線程之間競爭的時候界斜,則會升級為輕量級鎖,輕量級鎖的采用的是自旋鎖合冀,它默認在同步代碼塊消耗CPU時間不長的情況下各薇,不用去申請底層的Monitor鎖,在鎖被棧用的時候君躺,線程處于自選的狀態(tài)
輕量級鎖是采用自旋鎖的方式來實現(xiàn)的峭判,自旋鎖分為固定次數(shù)自旋鎖和自適應(yīng)自旋鎖。輕量級鎖是針對競爭鎖對象線程不多且線程持有鎖時間不長的場景棕叫。
重量級鎖:重量級鎖就是
JDK1.6
之前獲取Monitor的過程
4 synchronized對鎖的可重入性理解
- 可重入性針對的是線程對于鎖對象的獲取過程林螃,簡單理解為一個線程在拿到鎖對象的時候能否再次拿到這個鎖對象,可重入就表示可以再次拿到這個鎖對象俺泣,不可重入則表示不可以拿到這個鎖對象疗认,這樣就會發(fā)生死鎖的情況。
- 看下面的例子伏钠,正是由于java的內(nèi)置鎖是可重入的横漏,所以下面這段代碼不會發(fā)生死鎖:
public class Child extends Father{
public static void main(String[] args) {
new Child().doSomething();
}
public synchronized void doSomething(){
System.out.println("child");
super.doSomething();
}
}
class Father{
public synchronized void doSomething(){
System.out.println("Father");
}
}
首先執(zhí)行main方法,會執(zhí)行Child類中doSomething()
方法熟掂,該方法會拿到child對象的鎖缎浇。
然后執(zhí)行super.doSomething()
方法,又會重新獲取一下實例對象的鎖赴肚,注意素跺,這里在兩個類中的方法鎖對象均為(new Child()),所以這里就發(fā)生了重入鎖,不會造成死鎖的情況誉券。
- 重入鎖的一種實現(xiàn)方法是為每個鎖關(guān)聯(lián)一個線程持有者和計數(shù)器亡笑,當(dāng)計數(shù)器為0時表示該鎖沒有被任何線程持有,那么任何線程都可能獲得該鎖而調(diào)用相應(yīng)的方法横朋;當(dāng)某一線程請求成功后仑乌,JVM會記下鎖的持有線程,并且將計數(shù)器置為1琴锭;此時其它線程請求該鎖晰甚,則必須等待;而如果同一個線程再次請求這個鎖决帖,就可以再次拿到這個鎖厕九,同時計數(shù)器會遞增;當(dāng)線程退出同步代碼塊時地回,計數(shù)器會遞減扁远,如果計數(shù)器為0俊鱼,則釋放該鎖。
5 釋放鎖的時機
- 當(dāng)方法(代碼塊)執(zhí)行完畢后會自動釋放鎖畅买,不需要做任何的操作并闲。
- 當(dāng)一個線程執(zhí)行的代碼出現(xiàn)異常時,其所持有的鎖會自動釋放谷羞。
-------------------以下是補充內(nèi)容--------------------------
6 Java對象的內(nèi)存布局
參考 —— Java對象頭詳解
在JVM中帝火,對象在內(nèi)存中的布局分為三塊區(qū)域:對象頭、實例數(shù)據(jù)和對齊填充湃缎。
-
對象頭:對象頭分為
Mark Word
和Class Metadata Addresss
兩個部分犀填,Mark Word
存儲對象的hashCode、鎖信息或者分代年齡GC等標志等信息嗓违。Class Metadata Addresss
存放指向類元數(shù)據(jù)的指針九巡,JVM通過這個指針確定該對象是那個類的實列。 - 實例數(shù)據(jù):存放類的屬性數(shù)據(jù)信息蹂季,包括父類的屬性信息比庄,如果是數(shù)組的實例部分還包括數(shù)組的長度,這部分內(nèi)存按4字節(jié)對齊
- 對齊填充:由于虛擬機要求對象起始地址必須是8字節(jié)的整數(shù)倍乏盐。填充數(shù)據(jù)不是必須存在的佳窑,僅僅是為了字節(jié)對齊,這點了解即可父能。
1. 對象頭形式
前面已經(jīng)簡單說了對象頭中存儲的內(nèi)容神凑,這里詳細展開一下;JVM中對象頭的方式有以下兩種(以32位JVM為例):
- 普通對象
|--------------------------------------------------------------|
| Object Header (64 bits) |
|------------------------------------|-------------------------|
| Mark Word (32 bits) | Klass Word (32 bits) |
|------------------------------------|-------------------------|
- 數(shù)組對象
|---------------------------------------------------------------------------------|
| Object Header (96 bits) |
|--------------------------------|-----------------------|------------------------|
| Mark Word(32bits) | Klass Word(32bits) | array length(32bits) |
|--------------------------------|-----------------------|------------------------|
2 對象頭詳解
Mark Word:
這部分主要用來存儲對象自身的運行時數(shù)據(jù)何吝,如hashCode溉委、GC分代年齡等。mark word
的位長度為JVM的一個Word大小爱榕,也就是說32位JVM的Mark word
為32位瓣喊,64位JVM為64位。為了讓一個字大小存儲更多的信息黔酥,JVM將字的最低兩個位設(shè)置為標記位藻三,不同標記位下的Mark Word示意如下
|-------------------------------------------------------|--------------------|
| Mark Word (32 bits) | State |
|-------------------------------------------------------|--------------------|
| identity_hashcode:25 | age:4 | biased_lock:1 | lock:2 | Normal |
|-------------------------------------------------------|--------------------|
| thread:23 | epoch:2 | age:4 | biased_lock:1 | lock:2 | Biased |
|-------------------------------------------------------|--------------------|
| ptr_to_lock_record:30 | lock:2 | Lightweight Locked |
|-------------------------------------------------------|--------------------|
| ptr_to_heavyweight_monitor:30 | lock:2 | Heavyweight Locked |
|-------------------------------------------------------|--------------------|
| | lock:2 | Marked for GC |
|-------------------------------------------------------|--------------------|
這里以32位JVM為例,64位的情況下是一樣的跪者,32位JVM一個字是32為棵帽,64位JVM一個字是64為,JVM均用一個字的大小記錄當(dāng)前Mark Word中的信息渣玲。
-
Normal狀態(tài)下:正常無鎖的狀態(tài)
identity_hashcode
: 25位的對象標識Hash碼逗概,采用延遲加載技術(shù)。調(diào)用方法System.identityHashCode()
計算忘衍,并會將結(jié)果寫到該對象頭中逾苫。當(dāng)對象被鎖定時卿城,該值會移動到管程Monitor中。也就是說當(dāng)對象加鎖之后铅搓,前25位將不再是對象的hashCode瑟押。age:
4位的Java對象年齡。在GC中狸吞,如果對象在Survivor區(qū)復(fù)制一次勉耀,年齡增加1指煎。當(dāng)對象達到設(shè)定的閾值時蹋偏,將會晉升到老年代。默認情況下至壤,并行GC的年齡閾值為15威始,并發(fā)GC的年齡閾值為6。由于age只有4位像街,所以最大值為15黎棠,這就是-XX:MaxTenuringThreshold
選項最大值為15的原因。biased_lock:
對象是否啟用偏向鎖標記镰绎,只占1個二進制位脓斩。為1時表示對象啟用偏向鎖,為0時表示對象沒有偏向鎖畴栖。-
lock:
2位的鎖狀態(tài)標記位随静,由于希望用盡可能少的二進制位表示盡可能多的信息,所以設(shè)置了lock標記吗讶。該標記的值不同燎猛,整個mark word表示的含義不同。biased_lock lock· 狀態(tài) 0 01 無鎖 1 01 偏向鎖 0 00 輕量級鎖 0 10 重量級鎖 0 11 GC標記
-
Biased偏向鎖的狀態(tài):
-
thread:
23位表示持有偏向鎖的線程ID照皆。 -
epoch:
2位重绷,偏向時間戳,達到一定數(shù)量之后就會升級為輕量級鎖。 -
age
膜毁、biased_loc
昭卓、lock
跟無鎖的狀態(tài)一致。
-
-
Lightweight Locked輕量級鎖狀態(tài):
-
ptr_to_lock_record:
30位指向棧中鎖記錄的指針瘟滨。 - 剩下的兩位用于標志當(dāng)前的鎖狀態(tài)
-
-
Heavyweight Locked重量級鎖狀態(tài):
ptr_to_heavyweight_monitor:
30位指向管程Monitor的指針葬凳。剩下的兩位用于標志當(dāng)前的鎖狀態(tài)
Class_Metadata_Addresss:
這一部分用于存儲對象的類型指針,該指針指向它的類元數(shù)據(jù)室奏,JVM通過這個指針確定對象是哪個類的實例火焰。該指針的位長度為JVM的一個字大小,即32位的JVM為32位胧沫,64位的JVM為64位昌简。
注意: 這里在64位的JVM情況占业,可以出現(xiàn)指針壓縮,將類型指針壓縮成32位
array length:
如果對象是一個數(shù)組纯赎,那么對象頭還需要有額外的空間用于存儲數(shù)組的長度谦疾,這部分數(shù)據(jù)的長度也隨著JVM架構(gòu)的不同而不同:32位的JVM上,長度為32位犬金;64位JVM則為64位念恍。64位JVM如果開啟+UseCompressedOops
選項,該區(qū)域長度也將由64位壓縮至32位晚顷。
7 Monitor監(jiān)視器鎖
在上面對象頭分析中峰伙,可以看到當(dāng)對象頭鎖狀態(tài)位重量級鎖的時候,有一位ptr_to_heavyweight_monitor
空間该默,它存放的就是monitor對象(也稱為管理或監(jiān)視器鎖)的起始地址瞳氓。
每個對象都存在著一個 monitor 與之關(guān)聯(lián),對象與其 monitor 之間的關(guān)系有存在多種實現(xiàn)方式栓袖,如monitor可以與對象一起創(chuàng)建銷毀或當(dāng)線程試圖獲取對象鎖時自動生成匣摘,但當(dāng)一個 monitor 被某個線程持有后,它便處于鎖定狀態(tài)裹刮。
在Java虛擬機(HotSpot)中音榜,monitor是由ObjectMonitor
實現(xiàn)的,其主要數(shù)據(jù)結(jié)構(gòu)如下:
ObjectMonitor() {
_header = NULL;
_count = 0; //記錄個數(shù)
_waiters = 0,
_recursions = 0;
_object = NULL;
_owner = NULL;
_WaitSet = NULL; //處于wait狀態(tài)的線程捧弃,會被加入到_WaitSet
_WaitSetLock = 0 ;
_Responsible = NULL ;
_succ = NULL ;
_cxq = NULL ;
FreeNext = NULL ;
_EntryList = NULL ; //處于等待鎖block狀態(tài)的線程赠叼,會被加入到該列表
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;
}
這里我們主要關(guān)系幾個量:
_EntryList:
當(dāng)多個線程同時訪問一個同步代碼塊的時候,線程首先會進入EntryList
集合中塔橡,表示等待獲取鎖資源梅割。_WaitSet:
若線程調(diào)用 wait() 方法,將釋放當(dāng)前持有的monitor, 然后線程會進入WaitSet
,表示等待集合葛家。_owner:
指向持有ObjectMonitor
的線程_count:
count變量是實現(xiàn)鎖的可重入性的關(guān)鍵所在户辞,當(dāng)已經(jīng)擁有此ObjectMonitor
對象的線程再次請求獲取此對象的時候,鎖會判斷當(dāng)前_owner
,如果還是當(dāng)前的線程癞谒,則會在count位上加1底燎,實現(xiàn)鎖的可重入性。
簡單總結(jié)一下:
- monitor對象在每個Java對象的對象頭上面都有指向他的地址弹砚,synchronized就是通過這種方式實現(xiàn)鎖双仍。
8 synchronized 關(guān)鍵字的具體使用——手寫雙重檢驗鎖方式實現(xiàn)單例模式的原理
- 雙重校驗鎖實現(xiàn)對象單例(線程安全)
public class Singleton {
// 這里必須要加volatile修飾,在多線程的情況下可能會發(fā)生指令重排桌吃,如果不加此修飾朱沃,可能會出現(xiàn)創(chuàng) 建對象的可能
private volatile static Singleton singleton;
// 私有化構(gòu)造方法,只有他自己可以調(diào)用
private Singleton(){
}
// 創(chuàng)建靜態(tài)方法返回當(dāng)前的對象實列
public static Singleton getUniqueInstance(){
if(singleton == null){
// 如果檢查到當(dāng)前的singleton對象為空,則上鎖創(chuàng)建新對象逗物,其他線程不得入內(nèi)
synchronized (Singleton.class){
// 雙重判斷搬卒,避免另一個線程在創(chuàng)建完對象之后獲取了鎖進入當(dāng)前的同步代碼塊
if(singleton == null){
singleton = new Singleton();
}
}
}
return singleton;
}
}
- 第一次判空是在同步代碼塊之外, 如果當(dāng)前的singleton已經(jīng)常見出來了翎卓,不為null契邀,那么根本不會進入同步代碼塊。
- 第二部判空是在同步代碼塊內(nèi)部失暴,假設(shè)線程A搶到了鎖進入了同步方法坯门,還沒等到創(chuàng)建對象結(jié)束,線程B就執(zhí)行了同步代碼塊之外的判空指令逗扒,那么那就會等待線程A執(zhí)行完畢古戴,獲取鎖之后再繼續(xù)執(zhí)行。但是此時的線程A會進行new對象的操作缴阎,等到線程B拿到鎖進入同步代碼塊的時候允瞧,singleton對象已經(jīng)被new出來了简软,這里的二次判空操作使得線程B不會再去執(zhí)行new對象的操作蛮拔。
- 需要注意 singleton實列采用 volatile 關(guān)鍵字修飾也是很有必要。因為在new一個對象的時候痹升,代碼其實是分為三步執(zhí)行:
- 為singleton分配內(nèi)存空間
- 初始化singleton
- 將singleton指向分配的內(nèi)存空間
- 如果不加volatile 關(guān)鍵字修飾建炫,在多線程的情況下,很可能因為指令重排疼蛾,導(dǎo)致執(zhí)行順序變成了1-3-2肛跌,在這種情況下,線程獲取了還沒初始化的實列察郁,所以會產(chǎn)生問題衍慎,而volatile 關(guān)鍵字的最大功能之一就是防止指令重排序。
參考: