java synchronized原理
思考
- 當synchronized加的是偏向鎖或者輕量級鎖的時候掂榔,調(diào)用 wait方法會怎樣
- 對象的wait方法要依賴Monitor對象的實現(xiàn)败匹,而且需要有個隊列來存儲阻塞等待的線程是钥,偏向鎖和輕量級鎖都不涉及線程的阻塞,所以肖爵,我猜測會進行鎖膨脹為重量級鎖卢鹦,然后調(diào)用Monitor對象的wait方法
- 為什么重量級鎖叫鎖膨脹
- 重量級鎖會將對象頭Mark World指向一個Monitor對象,Monitor對象更像是對象頭的補充劝堪,在該對象中存儲了持有鎖的線程ID冀自、阻塞線程隊列、等待線程隊列秒啦,當這倆隊列中有信息時熬粗,這個Monitor對象就得一直掛在對象頭上。就像是對對象要實現(xiàn)鎖功能的補充余境,所以叫鎖膨脹
- 假如一個重入3次的線程調(diào)用wait方法驻呐,怎么處理
- 當前線程阻塞,并加入到Monitor的等待隊列猜拾,節(jié)點Node至少要有兩個信息挎袜,一個線程ID宋雏,一個重入次數(shù)
知識導讀
- synchronized方法依賴標記flag為ACC_SYNCHRONIZED,同步代碼塊依賴monitorenter和monitorexit指令
- synchronized在獲鎖的過程中是不能被中斷的笼沥,意思是說如果產(chǎn)生了死鎖奔浅,則不可能被中斷
- 同步方法汹桦、同步代碼塊中拋出異常舞骆,鎖自動釋放
- synchronized是可重入的非公平鎖實現(xiàn)
- 無鎖督禽、偏向鎖总处、輕量級鎖不會出現(xiàn)線程阻塞的情況鹦马,這也是JVM優(yōu)化的最重要的一個目的
- 無鎖荸频、偏向鎖、輕量級鎖蔑滓、自旋鎖键袱、重量級鎖的優(yōu)化場景及鎖升級過程
- 鎖對象的方法實現(xiàn)依賴Monitor對象的實現(xiàn)褐健,Monitor對象依賴操作系統(tǒng)的Mutex互斥鎖實現(xiàn)
- Monitor對象可以類比AQS蚜迅,封裝了持有鎖的線程,被阻塞線程隊列刹帕、等待線程隊列谎替、重入次數(shù)等信息
- java的線程是映射到操作系統(tǒng)的原生內(nèi)核線程之上的钱贯,如果要阻塞或喚醒一條線程挫掏,則需要操作系統(tǒng)來幫忙完成秩命,這就不可避免地陷入用戶態(tài)到核心態(tài)的轉(zhuǎn)換中,進行這種狀態(tài)轉(zhuǎn)換需要耗費很多的處理器時間硫麻。
- 計算過鎖對象的hashCode之后,對象頭上存儲了hashCode值拿愧,該鎖對象永遠無法作為偏向鎖了杠河,每次加鎖會直接走輕量級鎖或重量級鎖流程
對象鎖
Java 中的每一個對象都可以作為鎖
- 對于同步方法浇辜,鎖是當前實例對象柳洋。
- 對于同步方法塊卑雁,鎖是 Synchonized 括號里配置的對象。
- 對于靜態(tài)同步方法莹捡,鎖是當前對象的 Class 對象篮赢。
對象在內(nèi)存的分布分為3個部分:對象頭,實例數(shù)據(jù)琉挖,和對齊填充启泣。在對象頭中的Mark Word存儲了對象的鎖信息。Java中鎖對象有四種狀態(tài)示辈,無鎖狀態(tài)寥茫,偏向鎖狀態(tài),輕量級鎖狀態(tài)和重量級鎖狀態(tài)顽耳,它會隨著競爭情況逐漸升級坠敷。鎖可以升級但不能降級,目的是為了提高獲得鎖和釋放鎖的效率
注意:在偏向鎖射富、輕量級鎖、重量級鎖狀態(tài)下粥帚,對象頭的hashCode位置被鎖標志占用
- 輕量級鎖狀態(tài)下胰耗,HashCode會被復制到棧空間的Lock Record中
- 重量級鎖狀態(tài)下芒涡,HashCode會被存儲在對象頭關(guān)聯(lián)的Monitor對象中
- 偏向鎖狀態(tài)下沒有地方存儲HashCode柴灯,所以一旦遇到計算對象的hashCode,偏向鎖會升級到重量級鎖费尽。對象的hashCode是延遲計算的赠群,當一個對象已經(jīng)計算過hashCode,對象頭存儲了hashCode值旱幼,那么該對象鎖永遠無法作為偏向鎖使用了
原理
synchronized方法
javap查看synchronized方法會被編譯成方法的flags加上標志ACC_SYNCHRONIZED.
線程執(zhí)行方法時會檢查方法的ACC_SYNCHRONIZED
標志查描,如果設置了線程需要先去獲取對象鎖,執(zhí)行完畢后線程再釋放對象鎖柏卤,在方法執(zhí)行期間冬三,同一時刻只有一個線程能成功獲取鎖
public synchronized void test(){
System.out.println("test");
}
public synchronized void test();
descriptor: ()V
flags: ACC_PUBLIC, ACC_SYNCHRONIZED
Code:
stack=0, locals=1, args_size=1
0: return
synchronized代碼塊
javap查看同步塊的入口位置和退出位置分別插入monitorenter和monitorexit字節(jié)碼指令。原理同synchronized方法一致
public void test(){
synchronized (this) {
}
}
public void test();
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
內(nèi)存語義
- 當線程獲取鎖時缘缚,JMM會把該線程對應的本地內(nèi)存置為無效勾笆,同步代碼塊必須去主內(nèi)存讀取所需的共享變量。
- 當線程釋放鎖時桥滨,JMM會將當前線程工作內(nèi)存中的共享變量刷新到主內(nèi)存中
優(yōu)化
synchronized重量級鎖依賴操作系統(tǒng)的mutex互斥鎖實現(xiàn)窝爪,需要阻塞線程弛车,由于線程的阻塞和重啟涉及CPU內(nèi)核切換,非常耗費性能蒲每,Jdk1.6之后針對synchronized做了一些優(yōu)化纷跛,來降低線程阻塞的幾率,主要包括如鎖粗化啃勉、鎖消除忽舟、輕量級鎖、偏向鎖淮阐、適應性自旋叮阅、重量級鎖等技術(shù)來減少鎖操作的開銷。
無鎖
無鎖沒有對資源進行鎖定泣特,所有的線程都能訪問并修改同一個資源,但同時只有一個線程能修改成功状您。
無鎖的實現(xiàn)主要依賴CAS操作勒叠,一般在一個循環(huán)中不斷的進行CAS操作直到成功。循環(huán)次數(shù)過多會導致CPU負載升高
偏向鎖
當鎖對象第一次被線程獲取的時候,虛擬機將會把對象頭中的標志位設置為“01”柒桑、把偏向模式設置為“1”魁淳,表示進入偏向模式界逛。同時使用CAS操作把獲取到這個鎖的線程ID記錄在Mark Word之中息拜。如果CAS操作成功该溯,持有偏向鎖的線程以后每次獲取該鎖時狈茉,虛擬機都可以不再進行任何同步操作
優(yōu)化背景
偏向鎖的目的是消除無競爭情況下的加鎖操作氯庆。大多數(shù)情況下鎖不存在競爭羽莺,總是由同一個線程操作鎖盐固。如果在接下來的執(zhí)行過程中,該鎖沒有被其他的線程獲取,則持有偏向鎖的線程將永遠不需要再進行同步和CAS
加鎖
- 檢查鎖對象頭的Mark Word是否為鎖狀態(tài)是否是01,即無鎖或者偏向鎖
- 如果鎖狀態(tài)不是01,進入輕量級鎖或者重量級鎖加鎖流程
- 如果
Mark Word
存儲了hashCode派撕,不可偏向?qū)ο螅苯舆M入輕量級鎖加鎖流程 - 如果是01际跪,可偏向鎖姆打,檢查
Mark Word
儲存的偏向線程ID是否為當前線程ID- 如果存儲的偏向線程ID為空闲延,CAS將Mark Word偏向線程ID設置為本線程陆馁,CAS成功設置偏向鎖標志為1,獲取偏向鎖成功
- 如果是當前線程ID,偏向線程為當前線程互婿,獲取偏向鎖成功
- 如果對象頭存儲的偏向線程ID不是當前線程ID壮锻,進入偏向鎖撤銷流程
釋放鎖
偏向鎖使用了一種等到競爭出現(xiàn)才釋放鎖的機制,持有偏向鎖的線程不會主動釋放偏向鎖奏夫,需要等待其他線程來競爭的時候才會釋放偏向鎖呛哟。當有線程競爭偏向鎖時者娱,進入以下流程
- 當?shù)竭_全局安全點(在這個時間點上沒有字節(jié)碼正在執(zhí)行)時掛起持有偏向鎖的線程
- 當前線程檢查持有偏向鎖的線程是否還存活
- 偏向鎖線程不處于活動狀態(tài)增炭,將對象頭設置為無鎖狀態(tài),清除
Mark Word
中的偏向ID奈嘿,當前線程重新進入獲取偏向鎖流程 - 偏向鎖線程處于活動狀態(tài)袄膏,判斷當前線程是否還在同步代碼塊中占有鎖
- 偏向線程出了同步代碼塊码党,不競爭鎖了蒸健,將對象頭設置為無鎖狀態(tài)蘑秽,清除
Mark Word
中的偏向ID肠牲,當前線程重新進入獲取偏向鎖流程 - 偏向線程還持有鎖幼衰,發(fā)生競爭,升級為輕量級鎖
- 將
Mark Word
指向擁有偏向鎖線程的椬忽ǎ空間**的lock record - 將對象頭鎖狀態(tài)修改為
00
,升級為輕量級鎖 - 恢復被掛起的偏向線程渡嚣,持有偏向鎖的線程獲取執(zhí)行權(quán),當前線程CAS自旋獲取輕量級鎖
- 將
- 偏向線程出了同步代碼塊码党,不競爭鎖了蒸健,將對象頭設置為無鎖狀態(tài)蘑秽,清除
- 偏向鎖線程不處于活動狀態(tài)增炭,將對象頭設置為無鎖狀態(tài),清除
- 當前線程檢查持有偏向鎖的線程是否還存活
注意:在競爭激烈的時候,偏向鎖會成為一種累贅识椰,要頻繁的暫停線程绝葡,可以通過設置 -XX:UseBiasedLocking=false
關(guān)閉偏向鎖功能
輕量級鎖
在當前線程的棧幀中開辟一塊鎖記錄(Lock Record)的空間,用于存儲鎖對象目前的Mark Word的拷貝腹鹉,然后CAS嘗試將對象的Mark Word
指向Lock Record藏畅,就表示持有輕量級鎖
優(yōu)化背景
大部分占用鎖時間不會太長,通過短暫的自旋后可以獲取到鎖种蘸,避免線程阻塞和喚醒墓赴。輕量級鎖通過簡單的將對象頭指向持有鎖的棧來標記加鎖成,當發(fā)生競爭時航瞭,先自旋CAS更新Mark World
指向本線程來競爭鎖诫硕。
加鎖
- 判斷鎖對象
Mark Word
中存儲的鎖記錄是否指向當前線程棧幀- 如果指向當前線程棧幀,說明是重入鎖刊侯,當前線程直接獲取執(zhí)行權(quán)
- 如果沒有指向當前線程棧幀章办,說明其他線程已經(jīng)獲取了輕量級鎖。在當前線程棧幀中開辟鎖記錄空間滨彻,用于存放鎖對象中的
Mark Word
的拷貝(Displaced Mark Word),CAS將鎖對象的Mark Word
指向當前椗航欤空間的鎖記錄- CAS更新成功,成功獲取輕量級鎖亭饵,將
Mark Word
的鎖標志置為00休偶,執(zhí)行同步代碼塊 - CAS更新失敗,進入自旋鎖流程辜羊,當前線程嘗試使用自旋來獲取鎖踏兜,直到獲取到鎖或者升級為重量級鎖后阻塞當前線程
- CAS更新成功,成功獲取輕量級鎖亭饵,將
釋放鎖
- 從當前線程的棧幀中取出Displaced Mark Word存儲的鎖記錄的內(nèi)容
- 當前線程嘗試使用CAS將鎖記錄中復制的
Displaced Mark Word
替換到鎖對象中的Mark Word
中- CAS更新成功,則釋放鎖成功八秃,釋放輕量級鎖碱妆,將鎖標志位置為01無鎖狀態(tài)
- CAS更新失敗,對象頭已經(jīng)由其他競爭的線程修改為10昔驱,Mark World已經(jīng)膨脹指向Monitor對象疹尾,當前鎖升級為重量級鎖,所以當前線程需要執(zhí)行重量級鎖釋放鎖流程骤肛,請看下文
自旋鎖
線程的阻塞和喚醒需要從用戶態(tài)轉(zhuǎn)換到核心態(tài)纳本,這個狀態(tài)之間的轉(zhuǎn)換需要相對比較長的時間,時間成本相對較高腋颠,自旋鎖就是避免線程進入阻塞狀態(tài)饮醇。在大多數(shù)情況下,線程持有鎖的時間都不會太長秕豫,所以希望通過短時間的重復嘗試去獲取鎖,避免線程阻塞。
當線程在獲取輕量級鎖的過程中執(zhí)行CAS更新Mark World
失敗時,為了避免線程真實地在操作系統(tǒng)層面掛起,虛擬機通過自旋不斷嘗試CAS更新Mark World
契耿。
- 自旋若干次后吹散,CAS修改
Mark World
成功則成功獲取輕量級鎖,線程獲取執(zhí)行權(quán)告喊。 - 自旋若干次后還是獲取鎖失敗,當前線程進入阻塞狀態(tài),升級為重量級鎖
注意:自旋等待雖然避免了線程切換的開銷狗准,但它要占用處理器時間。如果鎖被占用的時間很短茵肃,自旋等待的效果就會非常好腔长。反之,如果鎖被占用的時間很長验残,那么自旋的線程只會白浪費處理器資源捞附。所以,自旋等待的時間必須要有一定的限度您没,如果自旋超過了限定次數(shù)(默認是10次鸟召,可以使用-XX:PreBlockSpin來更改)沒有成功獲得鎖,就應當掛起線程氨鹏。
自適應自旋鎖
自旋是需要消耗CPU性能的欧募,為了避免競爭激烈情況下無意義的自旋,JDK1.6引入自適應的自旋鎖仆抵,自適應就意味著自旋的次數(shù)不再是固定的跟继,它是由前一次在同一個鎖上的自旋時間及鎖的擁有者的狀態(tài)來決定。線程如果自旋成功了肢础,則下次自旋的次數(shù)會更多还栓,如果自旋失敗了,則自旋的次數(shù)就會減少传轰。
- 某個某個鎖對象自旋成功獲取輕量級鎖剩盒,并且持有鎖的線程正在運行中,那么虛擬機就會認為這次自旋也很有可能再次成功慨蛙,進而它將允許自旋持續(xù)相對更長的時間辽聊。
- 如果某個對象鎖自旋很少成功獲得,下次會減少自旋的次數(shù)期贫,很快升級為重量級鎖進入阻塞狀態(tài)跟匆,避免CPU資源浪費
重量級鎖
重量級鎖涉及到了線程的阻塞,所以需要有一個容器隊列來存儲所有阻塞的線程通砍。對象鎖還支持wait方法玛臂,允許持有鎖的線程釋放鎖并進入阻塞等待狀態(tài)烤蜕,也需要一個容器隊列來存儲所有阻塞等待的線程。Monitor就是做這件事的迹冤。通過將鎖對象頭的Mark World
指向Monitor對象讽营,實現(xiàn)對象頭的補充膨脹效果。Monitor對象更像是對對象頭的功能補充
重量級鎖的實現(xiàn)是依靠Monitor對象實現(xiàn)泡徙,Monitor的本質(zhì)是依賴于底層操作系統(tǒng)的MutexLock(互斥鎖)實現(xiàn)橱鹏,MutexLock會導致線程的阻塞和喚醒,操作系統(tǒng)實現(xiàn)線程之間的切換需要從用戶態(tài)到內(nèi)核態(tài)的轉(zhuǎn)換堪藐,成本非常高莉兰。同時Monitor維護了持有鎖線程、阻塞線程隊列礁竞、阻塞等待線程隊列糖荒、重入次數(shù)等信息
monitor鎖對象
monitor是線程私有的數(shù)據(jù)結(jié)構(gòu),存儲在棧中苏章,每一個線程都有一個可用monitor列表寂嘉,同時還有一個全局的可用列表,當線程可用monitor列表為空的時候會請求全局可用列表補充枫绅。泉孩??并淋?
Owner:初始時為NULL表示當前沒有任何線程擁有該monitor寓搬,線程加鎖成功后記錄持有鎖的線程ID,當鎖釋放后設置為NULL县耽;
EntryQ: 鏈表句喷,用于存儲所有阻塞在該鎖對象上的線程。存有兩個隊列兔毙,entry-set用于存儲正在阻塞競爭的線程唾琼,wait-set用于存儲調(diào)用wait方法等待喚醒的線程
RcThis: 表示所有阻塞或者等待在該鎖對象上的線程個數(shù)
Nest: 記錄鎖對象重入的次數(shù)
HashCode: 保存從對象頭拷貝過來的HashCode值
Candidate: 0表示沒有需要喚醒的線程1表示要喚醒一個繼任線程來競爭鎖。
對象鎖的API實現(xiàn)依賴Monitor的實現(xiàn)澎剥,Monitor提供了獲取鎖(enter)锡溯、釋放鎖(exit)、等待(wait)哑姚、notify(喚醒)祭饭、notifyAll(喚醒所有)功能
加鎖
線程CAS自旋獲取輕量級鎖失敗后,將鎖膨脹為重量級鎖
- 從當前線程可用Monitor列表中取出一個Monitor對象叙量,修改
Mark World
指向Monitor對象 - 修改Monitor對象的Owner為持有輕量級鎖的線程ID,Nest初始為1
- 修改對象頭的鎖狀態(tài)為重量級鎖(10)
- 當前線程加入到Monitor的entry-set隊列中倡蝙,進入阻塞狀態(tài)
當一個新的線程來競爭重量級鎖或者當阻塞隊列中的線程被喚醒,競爭重量級鎖
- 當前線程判斷對象頭的鎖狀態(tài)為重量級鎖(10)绞佩,然后根據(jù)
Mark World
引用獲取Monitor對象 - 當前線程調(diào)用Monitor對象的獲取鎖(enter)方法
- 如果Monitor鎖沒有被其他線程獲取寺鸥,當前線程成功獲取鎖猪钮,修改Monitro的Owner為當前線程
- 如果Monitor鎖已經(jīng)被其他線程獲取,判斷Owner是否為當前線程
- 如果相同胆建,重入鎖躬贡,Nest++,當前線程繼續(xù)持有鎖
- 如果不同眼坏,當前線程加入到entry-set中,進入阻塞狀態(tài)
釋放鎖
- 將Monitor對象的Next字段減去 0 ,判斷減去后的值是否為0(可能重入)
- Nest>0酸些,說明重入鎖還未釋放鎖宰译,當前線程繼續(xù)持有鎖
- Next=0, 重入鎖釋放完成,設置Owner為空魄懂,檢查rfThis是否大于0沿侈,用于判讀是否需要喚醒被阻塞的線程
- rfThis>0,喚醒阻塞隊列中一個被阻塞的線程市栗,該線程競爭重量級鎖
- rfThis=0缀拭,沒有其他線程在競爭鎖,也沒有阻塞中的線程了填帽,當前Monior已經(jīng)沒有用了蛛淋,徹底釋放鎖,將
Mark World
指向為null篡腌,設置鎖狀態(tài)為01(無鎖),將解除關(guān)聯(lián)的monitor對象重新放入線程可用monitor列表中
鎖粗化
在已經(jīng)持有該對象鎖的情況下褐荷,再次調(diào)用synchronized該對象,不再判斷嘹悼,如StringBuilder.append().apend()
public void test() {
synchronized (this) {
System.out.println("a");
}
synchronized (this) {//優(yōu)化后不再阻塞
System.out.println("b");
}
}
鎖消除
通過運行時JIT編譯器的逃逸分析來消除一些沒有操作共享變量的同步操作
public void test1() {
Vector vector = new Vector();
vector.add("1");//vector本身屬于線程私有的叛甫,調(diào)用add的時候會進行鎖消除
vector.add("2");
}