synchronized的實現(xiàn)原理是啥?
點擊上方藍色字體,選擇“標(biāo)星公眾號”
優(yōu)質(zhì)文章,第一時間送達
? 作者?|??夜勿語?
來源 |? urlify.cn/MvYZZj
前言
上一篇分析了優(yōu)化后的synchronized在不同場景下對象頭中的表現(xiàn)形式,還記得那個結(jié)論嗎?當(dāng)一個線程第一次獲取鎖后再去拿鎖就是偏向鎖,如果有別的線程和當(dāng)前線程交替執(zhí)行就膨脹為輕量級鎖,如果發(fā)生競爭就會膨脹為重量級鎖。這句話看起來很簡單,但實際上synhronized的膨脹過程是非常復(fù)雜的,有許多場景和細節(jié)需要考慮,本篇就對其進行詳細分析。
正文
先來看一個案例代碼:
public?class?TestInflate?{
????static?Thread?t2;
????static?Thread?t3;
????static?Thread?t1;
????static?int?loopFlag?=?19;
????public?static?void?main(String[]?args)?throws?InterruptedException?{
????????//a?沒有線程偏向---匿名????101偏向鎖
????????List?list?=?new?ArrayList<>();
????????t1?=?new?Thread()?{
????????????@Override
????????????public?void?run()?{
????????????????for?(int?i?=?0;?i?????????????????????A?a?=?new?A();
????????????????????list.add(a);
????????????????????synchronized?(a)?{
????????????????????????log.debug(i?+?"?"?+?ClassLayout.parseInstance(a).toPrintableTest(a));
????????????????????}
????????????????}
????????????????log.debug("========t2=================");
????????????????LockSupport.unpark(t2);
????????????}
????????};
????????t2?=?new?Thread()?{
????????????@Override
????????????public?void?run()?{
????????????????LockSupport.park();
????????????????for?(int?i?=?0;?i?????????????????????A?a?=?list.get(i);
????????????????????log.debug(i?+?"?"?+?ClassLayout.parseInstance(a).toPrintable(a));
????????????????????synchronized?(a)?{
????????????????????????log.debug(i?+?"?"?+?ClassLayout.parseInstance(a).toPrintable(a));
????????????????????}
????????????????????log.debug(i?+?"?"?+?ClassLayout.parseInstance(a).toPrintable(a));
????????????????}
????????????????log.debug("======t3=====================================");
????????????????LockSupport.unpark(t3);
????????????}
????????};
????????t3?=?new?Thread()?{
????????????@Override
????????????public?void?run()?{
????????????????LockSupport.park();
????????????????for?(int?i?=?0;?i?????????????????????A?a?=?list.get(i);
????????????????????log.debug(i?+?"?"?+?ClassLayout.parseInstance(a).toPrintable(a));
????????????????????synchronized?(a)?{
????????????????????????log.debug(i?+?"?"?+?ClassLayout.parseInstance(a).toPrintable(a));
????????????????????}
????????????????????log.debug(i?+?"?"?+?ClassLayout.parseInstance(a).toPrintable(a));
????????????????}
????????????}
????????};
????????t1.start();
????????t2.start();
????????t3.start();
????????t3.join();
????????log.debug(ClassLayout.parseInstance(new?A()).toPrintable());
????}
這里創(chuàng)建了三個線程t1、t2、t3,在t1中創(chuàng)建了loopFlag個對象并依次加鎖,然后放入到list中,t2等待t1執(zhí)行完成后依次讀取list中對象進行加鎖并打印加鎖前、加鎖后、解鎖后的對象頭,t3和t2相同,只不過需要等待t2執(zhí)行完才開始執(zhí)行,最后等三個線程執(zhí)行完成后再新建一個對象并打印對象頭(注意運行該代碼需要關(guān)閉偏向延遲-XX:BiasedLockingStartupDelay=0)。
偏向鎖
偏向鎖沒什么好演示的,但是在源碼中獲取偏向鎖是第一步,且邏輯比較多,有以下幾點需要注意:
是否已經(jīng)超過偏向延遲指定的時間,若沒有,則只能獲取輕量鎖
是否允許偏向
如果只有當(dāng)前線程且是第一次則直接獲取偏向鎖(使用class對象中的mark word和線程id做"或"操作,得到一個新的header,并通過CAS替換鎖對象頭,替換成功則獲取到偏向鎖,否則進入鎖升級的流程)
是否調(diào)用了鎖對象未重寫的hashcode(對應(yīng)源碼中的Object#hash或System.identityHashCode()方法),hashcode會占用對象頭的空間,導(dǎo)致無法偏向
線程是否交替執(zhí)行(即當(dāng)前線程ID和對象頭中的線程ID不一致),若是交替執(zhí)行可能獲取到偏向鎖、輕量鎖,細節(jié)下文詳細講述。
輕量鎖
首先注釋掉t3,先設(shè)置loopFlag=19運行t1和t2,你能猜到打印的對象頭是什么樣的么?(為節(jié)省篇幅,下文對象頭都只截取最后8位展示)
15:57:38.579?[Thread-0]?DEBUG?cn.dark.ex6.TestInflate?-?0?00000101
15:57:38.580?[Thread-0]?DEBUG?cn.dark.ex6.TestInflate?-?1?00000101
......
15:57:38.582?[Thread-0]?DEBUG?cn.dark.ex6.TestInflate?-?17?00000101
15:57:38.582?[Thread-0]?DEBUG?cn.dark.ex6.TestInflate?-?18?00000101
15:57:38.582?[Thread-0]?DEBUG?cn.dark.ex6.TestInflate?-?========t2=================
15:57:38.582?[Thread-1]?DEBUG?cn.dark.ex6.TestInflate?-?0?00000101
15:57:38.583?[Thread-1]?DEBUG?cn.dark.ex6.TestInflate?-?0?10000000
15:57:38.583?[Thread-1]?DEBUG?cn.dark.ex6.TestInflate?-?0?00000001
15:57:38.583?[Thread-1]?DEBUG?cn.dark.ex6.TestInflate?-?1?00000101
15:57:38.583?[Thread-1]?DEBUG?cn.dark.ex6.TestInflate?-?1?10000000
15:57:38.583?[Thread-1]?DEBUG?cn.dark.ex6.TestInflate?-?1?00000001
......
15:57:38.589?[Thread-1]?DEBUG?cn.dark.ex6.TestInflate?-?17?00000101
15:57:38.589?[Thread-1]?DEBUG?cn.dark.ex6.TestInflate?-?17?10000000
15:57:38.589?[Thread-1]?DEBUG?cn.dark.ex6.TestInflate?-?17?00000001
15:57:38.589?[Thread-1]?DEBUG?cn.dark.ex6.TestInflate?-?18?00000101
15:57:38.590?[Thread-1]?DEBUG?cn.dark.ex6.TestInflate?-?18?10000000
15:57:38.590?[Thread-1]?DEBUG?cn.dark.ex6.TestInflate?-?18?00000001
15:57:38.590?[Thread-1]?DEBUG?cn.dark.ex6.TestInflate?-?======t3=====================================
15:57:38.590?[main]?DEBUG?cn.dark.ex6.TestInflate?-?cn.dark.entity.A?object?internals:
?OFFSET??SIZE???TYPE?DESCRIPTION???????????????????????????????VALUE
??????0?????4????????(object?header)???????????????????????????05?00?00?00?(00000101?00000000?00000000?00000000)?(5)
??????4?????4????????(object?header)???????????????????????????00?00?00?00?(00000000?00000000?00000000?00000000)?(0)
??????8?????4????????(object?header)???????????????????????????2c?6a?01?f8?(00101100?01101010?00000001?11111000)?(-134125012)
?????12?????4????????(loss?due?to?the?next?object?alignment)
t1線程不用想,肯定都是101,因為拿到的是偏向鎖,但是t2就和我上一篇說的有點不一樣了。t2加鎖前的狀態(tài)和t1解鎖后是一樣的,偏向鎖解鎖不會改變對象頭,接著對其加鎖,判斷當(dāng)前線程id和對象頭中的線程id是否相同,由于不相同所以會做偏向撤銷(即將狀態(tài)修改為001無鎖狀態(tài))并膨脹為輕量鎖(實際上對象第一次加鎖時,也有這個判斷,接著會判斷是不是匿名偏向,即是不是可偏向模式且第一次加鎖,是則直接獲取偏向鎖),狀態(tài)改為00。
需要注意輕量鎖加鎖前會在當(dāng)前線程棧幀中創(chuàng)建一個無鎖的Lock Record,加鎖時就會使用CAS操作判斷當(dāng)前對象頭中的mark word是否和lr中的displaced word相等,由于都是001所以能加鎖成功,之后輕量鎖解鎖只需要將lr中的dr恢復(fù)到當(dāng)前對象頭中(001),這樣下一個線程才能對該對象再次加鎖。需要注意雖然輕量鎖解鎖后對象頭是001狀態(tài),但新建的對象依然是默認的101可偏向無鎖狀態(tài),正如上面最后一次打印。
批量重偏向
上面創(chuàng)建的19個對象在膨脹為輕量鎖的時候都會進行偏向撤銷,但是撤銷是有性能損耗的,所以JVM設(shè)置了一個閾值,當(dāng)撤銷達到20次的時候就會進行批量重偏向,該閾值可通過-XX:BiasedLockingBulkRebiasThreshold=20修改。
將上面代碼中的loopFlag改為大于19的數(shù)打印結(jié)果(后面都不再展示t1線程的打印結(jié)果):
16:52:02.005?[Thread-0]?DEBUG?cn.dark.ex6.TestInflate?-?========t2=================
16:52:02.005?[Thread-1]?DEBUG?cn.dark.ex6.TestInflate?-?0?00000101
16:52:02.005?[Thread-1]?DEBUG?cn.dark.ex6.TestInflate?-?0?00110000
16:52:02.005?[Thread-1]?DEBUG?cn.dark.ex6.TestInflate?-?0?00000001
......
16:52:02.011?[Thread-1]?DEBUG?cn.dark.ex6.TestInflate?-?18?00000101
16:52:02.012?[Thread-1]?DEBUG?cn.dark.ex6.TestInflate?-?18?00110000
16:52:02.012?[Thread-1]?DEBUG?cn.dark.ex6.TestInflate?-?18?00000001
16:52:02.012?[Thread-1]?DEBUG?cn.dark.ex6.TestInflate?-?19?00000101
16:52:02.012?[Thread-1]?DEBUG?cn.dark.ex6.TestInflate?-?19?00000101
16:52:02.012?[Thread-1]?DEBUG?cn.dark.ex6.TestInflate?-?19?00000101
16:52:02.012?[Thread-1]?DEBUG?cn.dark.ex6.TestInflate?-?20?00000101
16:52:02.012?[Thread-1]?DEBUG?cn.dark.ex6.TestInflate?-?20?00000101
16:52:02.012?[Thread-1]?DEBUG?cn.dark.ex6.TestInflate?-?20?00000101
16:54:45.035?[main]?DEBUG?cn.dark.ex6.TestInflate?-?cn.dark.entity.A?object?internals:
?OFFSET??SIZE???TYPE?DESCRIPTION???????????????????????????????VALUE
??????0?????4????????(object?header)???????????????????????????05?01?00?00?(00000101?00000001?00000000?00000000)?(261)
??????4?????4????????(object?header)???????????????????????????00?00?00?00?(00000000?00000000?00000000?00000000)?(0)
??????8?????4????????(object?header)???????????????????????????2c?6a?01?f8?(00101100?01101010?00000001?11111000)?(-134125012)
?????12?????4????????(loss?due?to?the?next?object?alignment)
前面19個對象都需要進行撤銷,當(dāng)達到20時,所有的對象頭都變成了101了,并且偏向當(dāng)前線程t2(這里需要注意,批量指的是當(dāng)前正被加鎖的所有對象,還沒有加鎖的,即從第21個對象開始都是逐個重偏向;另外雖重偏向是先將鎖對象設(shè)置為可偏向無鎖模式101,再講線程id設(shè)置進去),如果此時你打印完整的對象頭出來還會發(fā)現(xiàn)偏向時間戳標(biāo)志設(shè)置為了01,即代表過期進行了重偏向。需要注意,這時候新建的對象也是101狀態(tài),且是重偏向。
批量撤銷
JVM還有一個參數(shù)-XX:BiasedLockingBulkRevokeThreshold=40用來控制批量撤銷,即默認當(dāng)一個類累計撤銷達到40次,那么新建的對象就直接是無鎖不可偏向的,因為JVM認為這是代碼存在了嚴(yán)重的問題。
將t3注釋放開,并將loopFlag設(shè)置為50,觀察結(jié)果:
17:15:46.640?[Thread-1]?DEBUG?cn.dark.ex6.TestInflate?-?======t3=====================================
17:15:46.640?[Thread-2]?DEBUG?cn.dark.ex6.TestInflate?-?0?00000001
17:15:46.640?[Thread-2]?DEBUG?cn.dark.ex6.TestInflate?-?0?11100000
17:15:46.640?[Thread-2]?DEBUG?cn.dark.ex6.TestInflate?-?0?00000001
......
17:15:46.644?[Thread-2]?DEBUG?cn.dark.ex6.TestInflate?-?18?00000001
17:15:46.644?[Thread-2]?DEBUG?cn.dark.ex6.TestInflate?-?18?11100000
17:15:46.644?[Thread-2]?DEBUG?cn.dark.ex6.TestInflate?-?18?00000001
17:15:46.644?[Thread-2]?DEBUG?cn.dark.ex6.TestInflate?-?19?00000101
17:15:46.644?[Thread-2]?DEBUG?cn.dark.ex6.TestInflate?-?19?11100000
17:15:46.644?[Thread-2]?DEBUG?cn.dark.ex6.TestInflate?-?19?00000001
.......
17:15:46.650?[Thread-2]?DEBUG?cn.dark.ex6.TestInflate?-?39?00000101
17:15:46.650?[Thread-2]?DEBUG?cn.dark.ex6.TestInflate?-?39?11100000
17:15:46.651?[Thread-2]?DEBUG?cn.dark.ex6.TestInflate?-?39?00000001
......
17:15:46.652?[Thread-2]?DEBUG?cn.dark.ex6.TestInflate?-?49?00000101
17:15:46.652?[Thread-2]?DEBUG?cn.dark.ex6.TestInflate?-?49?11100000
17:15:46.653?[Thread-2]?DEBUG?cn.dark.ex6.TestInflate?-?49?00000001
?OFFSET??SIZE???TYPE?DESCRIPTION???????????????????????????????VALUE
??????0?????4????????(object?header)???????????????????????????01?00?00?00?(00000001?00000000?00000000?00000000)?(1)
??????4?????4????????(object?header)???????????????????????????00?00?00?00?(00000000?00000000?00000000?00000000)?(0)
??????8?????4????????(object?header)???????????????????????????2c?6a?01?f8?(00101100?01101010?00000001?11111000)?(-134125012)
?????12?????4????????(loss?due?to?the?next?object?alignment)
t3線程前面20個對象都是從001加鎖為輕量鎖,所以不用進行撤銷,而t2線程從第21個對象開始都是獲取的偏向鎖,所以,t3線程就需要從第21個對象開始撤銷,當(dāng)和其它所有線程對該類對象累計撤銷了40次后新建的對象都不能再獲取偏向鎖(這里博主是直接設(shè)置的50個對象,讀者可以設(shè)置40個對象來驗證),不過在此之前已經(jīng)獲取偏向鎖的對象還是要逐個撤銷。
但是系統(tǒng)是長期運行的,可能批量重偏向之后很久才會累計撤銷達到40次,比如一個月、一年甚至更久,這種情況下就沒有必要進行批量撤銷了,因此JVM提供了一個參數(shù)-XX:BiasedLockingDecayTime=25000,即默認距上一次批量重偏向超過25000ms后,計數(shù)器就會重置為0。下面是JVM關(guān)于這一點的源碼:
??//?當(dāng)前時間
??jlong?cur_time?=?os::javaTimeMillis();
??//?該類上一次批量撤銷的時間
??jlong?last_bulk_revocation_time?=?k->last_biased_lock_bulk_revocation_time();
??//?該類偏向鎖撤銷的次數(shù)
??int?revocation_count?=?k->biased_lock_revocation_count();
??//?BiasedLockingBulkRebiasThreshold是重偏向閾值(默認20),
??//?BiasedLockingBulkRevokeThreshold是批量撤銷閾值(默認40),
??// BiasedLockingDecayTime默認25000。
??if?((revocation_count?>=?BiasedLockingBulkRebiasThreshold)?&&
??????(revocation_count??BiasedLockingBulkRevokeThreshold)?&&
??????(last_bulk_revocation_time?!=?0)?&&
??????(cur_time?-?last_bulk_revocation_time?>=?BiasedLockingDecayTime))?{
????//?重置計數(shù)器
????k->set_biased_lock_revocation_count(0);
????revocation_count?=?0;
??}
具體案例很簡單,讀者們可以思考下怎么驗證這個結(jié)論。
重量鎖
由于synchronized是c++語言實現(xiàn)的,實現(xiàn)比較復(fù)雜,就不進行詳細的源碼分析了,下面只是對其實現(xiàn)原理的一個總結(jié)。另外重量鎖的實現(xiàn)原理和ReentrantLock的思想是一樣的,讀者們可以對比理解。
當(dāng)多個線程發(fā)生競爭的時候,synchronized就會膨脹為重量鎖,這時會創(chuàng)建一個ObjectMoitor對象,這個對象包含了三個由ObjectWaiter對象組成的隊列:cxq、EntryList、WaitSet,以及兩個字段owner和Read Thread。cxq和EntryList都是獲取鎖失敗用來存儲等待的線程的,WaitSet則是Java中調(diào)用wait方法進入阻塞的線程,owner指向當(dāng)前獲取鎖的線程,而Read Thread則表示從cxq和EntryList中挑選出來去搶鎖的線程,但由于是非公平鎖,所以不一定能搶到鎖。
在膨脹為重量鎖的時候若沒有獲取到鎖,不是立馬就阻塞未獲取到鎖的線程,因其是非公平鎖,首先會去嘗試加鎖,不管前面是否有線程等待(如果是公平鎖的話就會判斷是否有線程等待,有的話則直接入隊睡眠),如果加鎖失敗,synchronized還會采用自旋的方式去獲取鎖,JDK1.6之前是默認自旋10次后睡眠,而優(yōu)化之后引入了適應(yīng)性自旋,即JVM會根據(jù)各種情況動態(tài)改變自旋次數(shù):
如果平均負載小于CPU則一直自旋
如果有超過(CPU/2)個線程正在自旋,則后來線程直接阻塞
如果正在自旋的線程發(fā)現(xiàn)Owner發(fā)生了變化則延遲自旋時間(自旋計數(shù))或進入阻塞
如果CPU處于節(jié)電模式則停止自旋
自旋時間的最壞情況是CPU的存儲延遲(CPU A存儲了一個數(shù)據(jù),到CPU B得知這個數(shù)據(jù)直接的時間差)
自旋時會適當(dāng)放棄線程優(yōu)先級之間的差異
你可能會比較好奇為什么不一直采用自旋,因為自旋是會消耗CPU的,適合并發(fā)數(shù)不多或自旋次數(shù)少的情形,否則不如直接調(diào)用系統(tǒng)函數(shù)進入睡眠狀態(tài)。
所以當(dāng)自旋沒有獲取到鎖,則會將當(dāng)前線程添加到cxq隊列的隊首(注意在入隊后還會搶一次鎖,這就是非公平鎖的特點,盡可能的避免調(diào)用系統(tǒng)函數(shù)進入內(nèi)核態(tài)阻塞)并調(diào)用park函數(shù)睡眠。
park函數(shù)是基于pthread_mutex_lock函數(shù)實現(xiàn)的,而Java中的LockSupport.park則是基于pthread_cond_timedwait函數(shù),這兩個都是系統(tǒng)函數(shù),更底層則是通過futex實現(xiàn)(注意此處都是基于Linux系統(tǒng)討論,其它不同的操作系統(tǒng)有不同的實現(xiàn)方式),這里就不展開討論了。
需要注意線程一旦進入隊列后,執(zhí)行的順序就是固定了,因為在當(dāng)前持有鎖的線程釋放鎖后,會從隊列中喚醒最后入隊的線程,即一朝排隊,永遠排隊,所以公平鎖和非公平鎖的區(qū)別就體現(xiàn)在入隊前是否搶鎖(排除有新的線程來搶鎖的情況)。
所謂喚醒最后入隊的線程,其實就類似于棧,先睡眠的線程后喚醒,這點和ReentratLock是相反的,下面給出證明:
public?class?Demo2?{
????private?static?Demo2?lock?=?new?Demo2();
????public?static?void?main(String[]?args)?throws?InterruptedException?{
????????Thread[]?threads?=?new?Thread[10];
????????for?(int?i?=?0;?i?10;?i++)?{
????????????threads[i]?=?new?Thread(()?->?{
????????????????synchronized?(lock)?{
????????????????????log.info(Thread.currentThread().getName());
????????????????}
????????????});
????????}
????????synchronized?(lock)?{
????????????for?(Thread?thread?:?threads)?{
????????????????thread.start();
????????????????//?睡眠一下保證線程的啟動順序
????????????????Thread.sleep(100);
????????????}
????????}
????}
}
上面程序創(chuàng)建了10個線程,然后主線程拿到鎖后依次啟動10個線程,這10個線程內(nèi)又會分別去獲取鎖,因為被主線程占有,就會膨脹為重量鎖進入阻塞,最終打印結(jié)果如下:
16:25:49.877?[Thread-9]?INFO??cn.dark.mydemo.sync.Demo2?-?Thread-9
16:25:49.879?[Thread-8]?INFO??cn.dark.mydemo.sync.Demo2?-?Thread-8
16:25:49.879?[Thread-7]?INFO??cn.dark.mydemo.sync.Demo2?-?Thread-7
16:25:49.879?[Thread-6]?INFO??cn.dark.mydemo.sync.Demo2?-?Thread-6
16:25:49.879?[Thread-5]?INFO??cn.dark.mydemo.sync.Demo2?-?Thread-5
16:25:49.879?[Thread-4]?INFO??cn.dark.mydemo.sync.Demo2?-?Thread-4
16:25:49.879?[Thread-3]?INFO??cn.dark.mydemo.sync.Demo2?-?Thread-3
16:25:49.879?[Thread-2]?INFO??cn.dark.mydemo.sync.Demo2?-?Thread-2
16:25:49.879?[Thread-1]?INFO??cn.dark.mydemo.sync.Demo2?-?Thread-1
16:25:49.879?[Thread-0]?INFO??cn.dark.mydemo.sync.Demo2?-?Thread-0
可以看到10個線程并不是按照啟動順序執(zhí)行的,而是以相反的順序被喚醒并執(zhí)行。
以上就是Synchronized的膨脹過程以及底層的一些實現(xiàn)原理,最后我畫了一張synchronized鎖膨脹過程的圖幫助理解,有不對的地方歡迎指出:
總結(jié)
通過兩篇文章分析了synchronized的實現(xiàn)原理,可以看到要實現(xiàn)一把高性能的鎖是相當(dāng)復(fù)雜的,這也是為什么JDK1.6才對synchronized進行了優(yōu)化(大概也是迫于ReentratLock的壓力吧),優(yōu)化過后性能基本上和ReentrantLock差不多,只不過后者使用上更加靈活,支持更多的高級特性,但思想上其實都是一樣的(應(yīng)該都是借鑒了futex的實現(xiàn)原理)。
深刻理解synchronized的膨脹過程,不僅僅用于應(yīng)付面試,而是能夠更好的使用它進行并發(fā)編程,比如何時加鎖,何時使用無鎖的自旋鎖。另外在進行業(yè)務(wù)開發(fā)遇到類似場景時也可以借鑒其思想。
本篇文章參考了以下文章,最后在此表示感謝,讓我少走了很多彎路,也了解了很多底層知識。
linux內(nèi)核級同步機制--futex
死磕Synchronized底層實現(xiàn)--偏向鎖
死磕Synchronized底層實現(xiàn)--輕量級鎖
死磕Synchronized底層實現(xiàn)--重量級鎖
synchronized實現(xiàn)原理及其優(yōu)化-(自旋鎖,偏向鎖,輕量鎖,重量鎖)


??? ?
感謝點贊支持下哈?
