synchronized 原理 (偏向鎖、輕量級鎖、鎖膨脹、自旋)
synchronized 原理
Synchronized 是 Java 中用于實現(xiàn)線程同步的關(guān)鍵字,它可以用于方法或代碼塊。當一個方法或代碼塊被 synchronized 修飾時,它將在任意時刻只允許一個線程訪問,保證了多線程環(huán)境下的數(shù)據(jù)安全性。
synchronized可用于修飾對象或方法:
方法上的 synchronized
class Test{
public synchronized void test() {
// ...
}
}
// 等價于
class Test{
public void test() {
synchronized(this) {
// ...
}
}
}
class Test{
public synchronized static void test() {
// ...
}
}
// 等價于
class Test{
public static void test() {
synchronized(Test.class) {
// ...
}
}
}
對象上的 synchronized
synchronized(對象) // 線程1,線程2(blocked)
{
臨界區(qū)
}
static int counter = 0;
static final Object room = new Object();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
synchronized (room) {
counter++;
}
}
}, "t1");
Thread t2 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
synchronized (room) {
counter--;
}
}
}, "t2");
t1.start();
t2.start();
t1.join();
t2.join();
log.debug("{}",counter);
}
synchronized 實際是用對象鎖保證了臨界區(qū)內(nèi)代碼的原子性,臨界區(qū)內(nèi)的代碼對外是不可分割的,不會被線程切換所打斷。
偏向鎖
一開始沒有競爭時,加的是偏向鎖。 Java 6 中引入了偏向鎖來做進一步優(yōu)化:只有第一次使用 CAS 將線程 ID 設(shè)置到對象的 Mark Word 頭,之后發(fā)現(xiàn)這個線程 ID 是自己的就表示沒有競爭,不用重新 CAS。以后只要不發(fā)生競爭,這個對象就歸該線程所有
對象頭格式:

一個對象創(chuàng)建時:
- 如果開啟了偏向鎖(默認開啟),那么對象創(chuàng)建后,markword 值為 0x05 即最后 3 位為 101,這時它的 thread、epoch、age 都為 0
- 偏向鎖是默認是延遲的,不會在程序啟動時立即生效,如果想避免延遲,可以加 VM 參數(shù) -
XX:BiasedLockingStartupDelay=0來禁用延遲 - 如果沒有開啟偏向鎖,那么對象創(chuàng)建后,markword 值為 0x01 即最后 3 位為 001,這時它的 hashcode、 age 都為 0,第一次用到 hashcode 時才會賦值
撤銷 - 調(diào)用對象 hashCode
調(diào)用了對象的 hashCode,但偏向鎖的對象 MarkWord 中存儲的是線程 id,如果調(diào)用 hashCode 會導致偏向鎖被撤銷
- 輕量級鎖會在鎖記錄中記錄 hashCode
- 重量級鎖會在 Monitor 中記錄 hashCode
在調(diào)用 hashCode 后使用偏向鎖,記得去掉 -XX:-UseBiasedLocking
撤銷 - 其它線程使用對象
當有其它線程使用偏向鎖對象時,會將偏向鎖升級為輕量級鎖
撤銷 - 調(diào)用 wait/notify
偏向鎖或者輕量級鎖,在調(diào)用了 wait 方法之后均會直接膨脹為重量級鎖。具體可以查看下面這篇對重量級鎖底層源碼的解析,其中有提到:
如果線程獲得鎖后調(diào)用
Object#wait方法,則會將線程加入到WaitSet中,當被Object#notify喚醒后,會將線程從WaitSet移動到cxq或EntryList中去。需要注意的是,當調(diào)用一個鎖對象的wait或notify方法時,如當前鎖的狀態(tài)是偏向鎖或輕量級鎖則會先膨脹成重量級鎖。
批量重偏向
如果對象雖然被多個線程訪問,但沒有競爭,這時偏向了線程 T1 的對象仍有機會重新偏向 T2,重偏向會重置對象 的 Thread ID
當撤銷偏向鎖閾值超過 20 次后,jvm 會這樣覺得,我是不是偏向錯了呢,于是會在給這些對象加鎖時重新偏向至加鎖線程
批量撤銷
當撤銷偏向鎖閾值超過 40 次后,jvm 會這樣覺得,自己確實偏向錯了,根本就不該偏向。于是整個類的所有對象 都會變?yōu)椴豢善虻模陆ǖ膶ο笠彩遣豢善虻?/p>
參考資料 https://github.com/farmerjohngit/myblog/issues/12 http://www.rzrgm.cn/LemonFive/p/11246086.html http://www.rzrgm.cn/LemonFive/p/11248248.html 偏向鎖論文
輕量級鎖
輕量級鎖的使用場景:如果一個對象雖然有多線程要加鎖,但加鎖的時間是錯開的(也就是沒有競爭),那么可以使用輕量級鎖來優(yōu)化。
輕量級鎖對使用者是透明的,即語法仍然是 synchronized
假設(shè)有兩個方法同步塊,利用同一個對象加鎖
- 創(chuàng)建鎖記錄(Lock Record)對象,每個線程都的棧幀都會包含一個鎖記錄的結(jié)構(gòu),內(nèi)部可以存儲鎖定對象的 Mark Word
- 讓鎖記錄中 Object reference 指向鎖對象,并嘗試用 cas 替換 Object 的 Mark Word,將 Mark Word 的值存 入鎖記錄
- 如果 cas 替換成功,對象頭中存儲了 鎖記錄地址和狀態(tài) 00 ,表示由該線程給對象加鎖
- 如果 cas 失敗,有兩種情況
- 如果是其它線程已經(jīng)持有了該 Object 的輕量級鎖,這時表明有競爭,進入鎖膨脹過程
- 如果是自己執(zhí)行了 synchronized 鎖重入,那么再添加一條 Lock Record 作為重入的計數(shù)
- 當退出 synchronized 代碼塊(解鎖時)如果有取值為 null 的鎖記錄,表示有重入,這時重置鎖記錄,表示重入計數(shù)減一
- 當退出 synchronized 代碼塊(解鎖時)鎖記錄的值不為 null,這時使用 cas 將 Mark Word 的值恢復給對象頭
- 成功,則解鎖成功
- 失敗,說明輕量級鎖進行了鎖膨脹或已經(jīng)升級為重量級鎖,進入重量級鎖解鎖流程
鎖膨脹
如果在嘗試加輕量級鎖的過程中,CAS 操作無法成功,這時一種情況就是有其它線程為此對象加上了輕量級鎖(有競爭),這時需要進行鎖膨脹,將輕量級鎖變?yōu)橹亓考夋i。
- 當 Thread-1 進行輕量級加鎖時,Thread-0 已經(jīng)對該對象加了輕量級鎖
- 這時 Thread-1 加輕量級鎖失敗,進入鎖膨脹流程
- 即為 Object 對象申請 Monitor 鎖,讓 Object 指向重量級鎖地址
- 然后自己進入
Monitor的EntryList BLOCKED
- 當 Thread-0 退出同步塊解鎖時,使用 cas 將 Mark Word 的值恢復給對象頭,失敗。這時會進入重量級解鎖 流程,即按照 Monitor 地址找到 Monitor 對象,設(shè)置 Owner 為 null,喚醒 EntryList 中 BLOCKED 線程
(對于對象的hashcode,輕量級鎖會將存儲在棧幀的鎖記錄里 重量級鎖存儲在monitor里)
自旋優(yōu)化
重量級鎖競爭的時候,還可以使用自旋來進行優(yōu)化,如果當前線程自旋成功(即這時候持鎖線程已經(jīng)退出了同步塊,釋放了鎖),這時當前線程就可以避免阻塞。
-
自旋會占用 CPU 時間,單核 CPU 自旋就是浪費,多核 CPU 自旋才能發(fā)揮優(yōu)勢。
-
在 Java 6 之后自旋鎖是自適應的,比如對象剛剛的一次自旋操作成功過,那么認為這次自旋成功的可能性會高,就多自旋幾次;反之,就少自旋甚至不自旋,總之,比較智能。
-
Java 7 之后不能控制是否開啟自旋功能
總結(jié)
鎖的升級流程:無鎖 --> 偏向鎖 --> (有競爭時先進行一次CAS,失敗后鎖膨脹)重量級鎖

浙公網(wǎng)安備 33010602011771號