在java多線程并發(fā)編程中,Synchronized
一直占有很重要的角色猛遍。Synchronized
通過獲取鎖來實(shí)現(xiàn)同步。先來看一下,它的使用方法:
package com.Vinctor.Tst;
public class VinctorSyncDemo {
public static synchronized void staticSyncMethod() {
System.out.println("static synchronized");
}
public synchronized void normalSyncMethod() {
System.out.println("normal SyncMethod");
}
public void normalInnerMethod() {
synchronized (this) {
}
}
public void staticInnerMethod() {
synchronized (VinctorSyncDemo.class) {
}
}
}
如上,分為三種方式:
- 如
staticSyncMethod
懊烤,修飾靜態(tài)方法梯醒,鎖是當(dāng)前類的類對象
,位于方法區(qū) - 如
normalSyncMethod
腌紧,修飾普通實(shí)例方法茸习,鎖是當(dāng)前實(shí)例對象
,位于堆 - 修飾代碼塊壁肋,如
normalInnerMethod
與staticInnerMethod
号胚,其中synchronized (this)
與normalSyncMethod
效果相同,synchronized (VinctorSyncDemo.class)
與staticSyncMethod
效果相同浸遗。
我們將上述代碼javap
之后猫胁,看一下字節(jié)碼指令:
可以看到看到staticInnerMetho
方法在執(zhí)行到synchronized代碼塊時(shí),有兩個(gè)指令monitorenter
和monitorexit
跛锌,而下面異常表指向的位置10弃秆,也同樣執(zhí)行了monitorexit
∷杳保可見同步代碼塊的實(shí)現(xiàn)是使用monitorenter
和monitorexit
兩個(gè)代碼塊進(jìn)行獲取鎖已釋放鎖的菠赚,發(fā)生異常之后,也同樣會(huì)釋放鎖氢卡。JVM 必須保證monitorenter
有monitorexit
相對應(yīng)锈至。當(dāng)監(jiān)視器一個(gè)線程被持有時(shí)晨缴,那么這個(gè)線程就持有了鎖译秦,其他線程就不能獲取這個(gè)監(jiān)視器,直至monitorexit
釋放鎖击碗。同步方法同樣也可以用著兩個(gè)指令獲取和釋放鎖筑悴。
當(dāng)一個(gè)線程試圖訪問同步方法或者同步代碼塊時(shí),首先需要獲取鎖稍途,退出同步方法或者同步代碼塊時(shí)需要釋放鎖阁吝。可以看出鎖在Synchronized
中起到至關(guān)重要的作用械拍。鎖是什么呢突勇?他是怎么存儲(chǔ)的呢?
通過以前的文章坷虑,我們了解到實(shí)例對象存儲(chǔ)在堆中甲馋,類對象存儲(chǔ)在方法中,一個(gè)對象區(qū)域包括:對象頭迄损,對象數(shù)據(jù)定躏,對其數(shù)據(jù)。此處對象頭中存儲(chǔ)著Synchronized的鎖。對象頭中有一個(gè)稱為Mark Word
的區(qū)域痊远,存儲(chǔ)著對象的 hashCode垮抗,分代年齡和鎖標(biāo)記位。如圖:
運(yùn)行期間碧聪,mark word 中存儲(chǔ)的數(shù)據(jù)鎖的標(biāo)記位的變化而變化冒版,其可能變化情況如下:
可以看到,分為很多鎖:偏向鎖逞姿,輕量級(jí)鎖壤玫,重量級(jí)鎖。
鎖的分類
內(nèi)置鎖按照狀態(tài)分為:無鎖狀態(tài)哼凯,偏向鎖欲间,輕量級(jí)鎖,重量級(jí)鎖断部。四中狀態(tài)隨著鎖的競爭加劇而升級(jí)猎贴,但是不是回退降級(jí)。(本文僅討論 HotSpot)
鎖的升級(jí)
虛擬機(jī)開發(fā)人員研究發(fā)現(xiàn)蝴光,大多數(shù)情況下她渴,多線程存在競爭的情況很少,為了避免同一線程多次進(jìn)行鎖的獲取蔑祟,故引入了偏向鎖的概念趁耗。
當(dāng)一個(gè)線程A訪問同步代碼塊的時(shí)候,
首先檢查鎖標(biāo)志位:如果為01(無鎖或偏向鎖)
疆虚,再檢查是否為偏向鎖苛败,
- 如果為
0(不是偏向鎖)
,會(huì)通過 CAS 操作(下面將解釋)在對象頭中存儲(chǔ)當(dāng)前訪問的線程 A 的ID 径簿; - 如果為
1(是偏向鎖)
罢屈,檢查一下對象頭中是否是當(dāng)前線程A 的 ID,如果是篇亭,則表示獲取到鎖缠捌,執(zhí)行代碼塊。接下文译蒂,
接上文曼月,如果不是當(dāng)前線程A 的 ID,則嘗試使用 CAS替換線程 ID柔昼,如果成功哑芹,則表示獲取鎖;如果失敗岳锁,則表示其他線程正在持有鎖绩衷,這時(shí)出現(xiàn)了競爭蹦魔。我們假設(shè)當(dāng)前線程 B 持有該鎖。出現(xiàn)了競爭咳燕,我們這時(shí)需要撤銷線程 B 的偏向鎖(解鎖)勿决。
當(dāng)線程 B 運(yùn)行到安全點(diǎn)或者安全區(qū)域的時(shí)候,線程 B 暫停招盲,這是檢查線程 B是否已經(jīng)退出了同步代碼塊低缩, - 如果線程 B已經(jīng)退出了同步代碼塊,則解鎖曹货,將對象頭 的線程 ID 清空咆繁,是否偏向鎖標(biāo)記位0(設(shè)為無鎖狀態(tài)),線程 A 繼續(xù)通過 CAS 獲取鎖顶籽。
- 如果線程 B 沒有退出同步代碼塊玩般,則表示線程 A 不能獲取鎖,這時(shí)鎖升級(jí)礼饱,升級(jí)為
輕量級(jí)鎖
坏为。
鎖升級(jí)輕量級(jí)鎖之后,對象頭中不在存儲(chǔ)線程 ID 等信息镊绪,而是將這些信息拷貝至持有鎖的線程棧中鎖記錄中匀伏,再將對象頭指向該地址。上面的例子??中蝴韭,
- 線程 B 持有鎖够颠,在線程 B 的棧中分配鎖記錄,并將對象頭數(shù)據(jù)拷貝進(jìn)去榄鉴,這時(shí)鎖的標(biāo)志位為
00(輕量級(jí)鎖)
履磨,并將對象頭指針指向該線程 B 的棧,線程 B 喚醒牢硅,并繼續(xù)執(zhí)行代碼蹬耘; - 此時(shí)線程 A 也是分配鎖記錄,并拷貝對象頭中 Mark Word减余,但是線程 A 還是不能獲取到鎖。
這時(shí)線程 A還是進(jìn)行 CAS 操作惩系,企圖將對象頭指向自己的鎖記錄位岔, - 如果替換成功,則表示線程 A 獲取到了鎖堡牡,執(zhí)行同步代碼塊抒抬;
- 如果還是不成功,這時(shí)線程 A 將執(zhí)行
自旋
CAS(自旋:顧名思義晤柄,自己轉(zhuǎn)著玩兒擦剑,也就是線程 A 不暫停等待,也不阻塞,而是執(zhí)行一些無用的代碼惠勒,此時(shí)空轉(zhuǎn)赚抡,也占用 CPU 時(shí)間,可以想象一個(gè)while 循環(huán)纠屋,目的就是稍等一下涂臣,看看持有鎖的線程是是否很快就釋放鎖),自旋的過程中嘗試 CAS 替換對象頭指針售担,當(dāng)自旋一定數(shù)目之后赁遗,線程 A 還是沒有獲取到鎖(夠悲催的),這時(shí)鎖升級(jí)族铆,升級(jí)為重量級(jí)鎖岩四,此時(shí)標(biāo)志位10
。升級(jí)為重量級(jí)鎖之后哥攘,線程 A 這是不在爭搶資源炫乓,而是掛起當(dāng)前線程,等待其他線程釋放鎖之后將它喚醒献丑。
擁有輕量級(jí)鎖的線程執(zhí)行完同步代碼塊后末捣,需要解鎖輕量級(jí)鎖,這是還是需要使用 CAS 操作创橄,將棧中鎖記錄替換會(huì)對象頭箩做,如果成功,表示沒有競爭妥畏;如果失敗邦邦,表示存在競爭,其他線程嘗試獲取過鎖醉蚁,那就需要在釋放鎖的過程中燃辖,喚醒被掛起的線程。
貼一張收藏的流程圖(出處不明网棍,如侵權(quán)黔龟,請告知):
一個(gè)??
以上就是鎖升級(jí)的過程滥玷,比較亂氏身,舉個(gè)現(xiàn)實(shí)生活的栗子,上廁所惑畴。廁所幾位鎖(對象)蛋欣。
為了方便對比,我們定一個(gè)規(guī)則如贷,當(dāng)一個(gè)人上廁所時(shí)陷虎,需要將自己的牌子掛在廁所的門上到踏,上完廁所,從廁所出來就不必摘下牌子尚猿,下次再上的時(shí)候就不需要掛了窝稿,只是看一下這個(gè)牌子是不是自己的就可以了。此為偏向鎖谊路。
當(dāng)你再上廁所的過程中讹躯,小明過來了也想上廁所,就看到了牌子缠劝,不是自己的潮梯,想要把牌子換成自己的,但是這是你還在上廁所惨恭,沒辦法秉馏,小明只能在廁所門前晃來晃去,等著你上完廁所出來脱羡。如果你能在短時(shí)間里出來萝究,那小明就進(jìn)去上廁所了。此時(shí)為輕量級(jí)鎖锉罐。
但是萬一你鬧肚子帆竹,小明也不可能一直在外面等著,這是他就選擇回座位等著脓规,并告訴你一聲:“哥們栽连,上完告訴我一聲!”侨舆,這時(shí)的小明不在主動(dòng)去想要獲取鎖秒紧,而是等著你上完廁所出來喊他,他才上廁所挨下。這是即為重量級(jí)鎖熔恢。
CAS
CAS,Compare and Swap即比較并替換臭笆,設(shè)計(jì)并發(fā)算法時(shí)常用到的一種技術(shù)叙淌。java 中的原子類以及concurrent包大量使用了該技術(shù)進(jìn)行原子操作。
CAS有三個(gè)操作數(shù):內(nèi)存值V耗啦、舊的預(yù)期值A(chǔ)凿菩、要修改的值B,當(dāng)且僅當(dāng)預(yù)期值A(chǔ)和內(nèi)存值V相同時(shí)帜讲,將內(nèi)存值修改為B并返回true,否則什么都不做并返回false
JDK中有一個(gè)類Unsafe(sun.misc.Unsafe)椒拗,它提供了硬件級(jí)別的原子操作似将。Unsafe類中的一個(gè)方法如下:
public final native boolean compareAndSwapInt(Object o, long offset,
int expected,
int x);
此為 native 方法获黔,JVM 會(huì)將此方法映射為cmpxchg
CPU 指令,該指令為原子操作在验,故多用于多線程環(huán)境中而不會(huì)產(chǎn)生數(shù)據(jù)錯(cuò)誤玷氏。