- 在多線程并發(fā)編程中Synchronized一直是元老級角色,很多人都會稱呼它為重量級鎖, 但是隨著Java SE1.6對Synchronized進行了各種優(yōu)化之后,有些情況下它并不那么重了,Java SE1.6中 為了減少獲得鎖和釋放鎖帶來的性能消耗而引入的偏向鎖和輕量級鎖。
- 在Java SE1.6中,鎖一共有4種狀態(tài),級別從低到高依次是:無鎖狀態(tài)、偏向鎖狀態(tài)、輕量級鎖狀態(tài)和重量級鎖狀態(tài)。
- 這幾個狀態(tài)會隨著鎖的競爭情況逐漸升級。鎖可以升級但是不能降級,這種策略是為了提高獲得鎖和釋放鎖的效率。
- 重量級鎖是悲觀鎖的一種,自旋鎖、輕量級鎖與偏向鎖屬于樂觀鎖。
Java線程阻塞的代價
上下文切換
- 單核處理器也支持多線程執(zhí)行代碼,CPU通過給每個線程分配CPU時間片來實現(xiàn)這個機制。
- 時間片:是CPU分配給各個線程的時間,因為時間片非常短,所以CPU通過不斷地切換線程執(zhí)行,讓我們覺得多個線程是同時進行的。
- CPU通過時間片分配算法來執(zhí)行任務,當前任務執(zhí)行一個時間片后會切換到下一個任務。但是,在切換前會保存上一個任務的狀態(tài),以便下次切換回這個任務的時候,可以再加載這個任務的狀態(tài)。任務從保存到再加載的過程就是一次上下文切換。
代價
- java的線程是映射到操作系統(tǒng)原生線程之上的,如果要阻塞或喚醒一個線程就需要操作系統(tǒng)介入,需要在用戶態(tài)與核心態(tài)之間切換,這種切換會消耗大量的系統(tǒng)資源,因為用戶態(tài)與內核態(tài)都有各自專用的內存空間,專用的寄存器等,用戶態(tài)切換至內核態(tài)需要傳遞給許多變量、參數(shù)給內核,內核也需要保護好用戶態(tài)在切換時的一些寄存器值、變量等,以便內核態(tài)調用結束后切換回用戶態(tài)繼續(xù)工作。
- 頻繁的上下文切換很費時,如果同步代碼執(zhí)行所需時間比上下文切換時間都要短,那引入重量級鎖切換上下文這種同步策略是失敗的:所以synchronized從JKD1.6進行了改進,引入了偏向鎖、輕量級鎖。
偏向鎖(Biased Locking)
- 偏向鎖會偏向于第一個訪問鎖的線程,如果在運行過程中,同步鎖只有一個線程訪問,不存在多線程競爭的情況,這時就會給該線程加一個偏向鎖。
- 如果在運行的過程中,遇到了其他線程搶占鎖,則持有偏向鎖的線程將會被掛起,JVM會消除它身上的偏向鎖,將鎖膨脹成為輕量級鎖。
加鎖過程
- 訪問Mark Word中偏向鎖的標識是否設置成1,鎖標志位是否為01,確認為可偏向狀態(tài)。
- 如果為可偏向狀態(tài),則測試線程ID是否指向當前線程,如果是,進入步驟5,否則進入步驟3。
- 如果線程ID并未指向當前線程,則通過CAS操作競爭鎖。如果競爭成功,則將Mark Word中線程ID設置為當前線程ID,然后執(zhí)行5;如果競爭失敗,執(zhí)行4。
- 如果CAS獲取偏向鎖失敗,則表示有競爭。當?shù)竭_全局安全點(safepoint)時獲得偏向鎖的線程被掛起,偏向鎖升級為輕量級鎖,然后被阻塞在安全點的線程繼續(xù)往下執(zhí)行同步代碼。(撤銷偏向鎖的時候會導致stop the word)
- 執(zhí)行同步代碼。
釋放
- 偏向鎖只有遇到其他線程嘗試競爭偏向鎖時,持有偏向鎖的線程才會釋放鎖,否則線程不會主動去釋放偏向鎖。偏向鎖的撤銷,需要等待全局安全點(在這個時間點上沒有字節(jié)碼正在執(zhí)行),它會首先暫停擁有偏向鎖的線程,判斷鎖對象是否處于被鎖定狀態(tài),撤銷偏向鎖后恢復到未鎖定(標志位為“01”)或膨脹成輕量級鎖(標志位為“00”)的狀態(tài)。
應用場景
- 始終只有一個線程在執(zhí)行同步塊,在它沒有執(zhí)行完釋放鎖之前,沒有其它線程去執(zhí)行同步塊,在鎖無競爭的情況下使用,一旦有了競爭就升級為輕量級鎖,升級為輕量級鎖的時候需要撤銷偏向鎖,撤銷偏向鎖的時候會導致stop the word操作。在有鎖的競爭時,偏向鎖會多做很多額外操作,尤其是撤銷偏向所的時候會導致進入安全點,安全點會導致stw,導致性能下降,這種情況下應當禁用;
輕量級鎖
- 輕量級鎖是由偏向所升級來的,偏向鎖運行在一個線程進入同步塊的情況下,當?shù)诙€線程加入鎖爭用的時候,偏向鎖就會升級為輕量級鎖;
加鎖過程
-
在代碼進入同步塊的時候,如果同步對象鎖狀態(tài)為無鎖狀態(tài)(鎖標志位為“01”狀態(tài),是否為偏向鎖為“0”),虛擬機首先將在當前線程的棧幀中建立一個名為鎖記錄(Lock Record)的空間,用于存儲鎖對象目前的Mark Word的拷貝,官方稱之為 Displaced Mark Word。這時候線程堆棧與對象頭的狀態(tài)如圖:
image 拷貝對象頭中的Mark Word復制到鎖記錄中;
拷貝成功后,虛擬機將使用CAS操作嘗試將對象的Mark Word更新為指向Lock Record的指針,并將Lock record里的owner指針指向object mark word。如果更新成功,則執(zhí)行步驟4,否則執(zhí)行步驟5。
-
如果這個更新動作成功了,那么這個線程就擁有了該對象的鎖,并且對象Mark Word的鎖標志位設置為“00”,即表示此對象處于輕量級鎖定狀態(tài),這時候線程堆棧與對象頭的狀態(tài)如圖所示。
image 如果這個更新操作失敗了,虛擬機首先會檢查對象的Mark Word是否指向當前線程的棧幀,如果是就說明當前線程已經擁有了這個對象的鎖,那就可以直接進入同步塊繼續(xù)執(zhí)行。否則說明鎖已經被別的線程持有,此時有多個線程同時競爭鎖,輕量級鎖就要膨脹為重量級鎖,鎖標志的狀態(tài)值變?yōu)椤?0”,Mark Word中存儲的就是指向重量級鎖(互斥量)的指針,后面等待鎖的線程也要進入阻塞狀態(tài)。 而當前線程便嘗試使用自旋來獲取鎖,自旋就是為了不讓線程阻塞,而采用循環(huán)去獲取鎖的過程。
輕量級鎖的釋放
- 釋放鎖線程視角:由輕量鎖切換到重量鎖,是發(fā)生在輕量鎖釋放鎖的期間,之前在獲取鎖的時候它拷貝了鎖對象頭的markword,在釋放鎖的時候如果它發(fā)現(xiàn)在它持有鎖的期間有其他線程來嘗試獲取鎖了,并且該線程對markword做了修改,兩者比對發(fā)現(xiàn)不一致,則切換到重量鎖。因為重量級鎖被修改了,所有display mark word和原來的markword不一樣了。怎么補救,就是進入mutex前,compare一下obj的markword狀態(tài)。確認該markword是否被其他線程持有。此時如果線程已經釋放了markword,那么通過CAS后就可以直接進入線程,無需進入mutex,就這個作用。
- 嘗試獲取鎖線程視角:如果線程嘗試獲取鎖的時候,輕量鎖正被其他線程占有,那么它就會修改markword,修改重量級鎖,表示該進入重量鎖了。還有一個注意點:等待輕量鎖的線程不會阻塞,它會一直自旋等待鎖,并如上所說修改markword。這就是自旋鎖,嘗試獲取鎖的線程,在沒有獲得鎖的時候,不被掛起,而轉而去執(zhí)行一個空循環(huán),即自旋。在若干個自旋后,如果還沒有獲得鎖,則才被掛起,獲得鎖,則執(zhí)行代碼。
自旋鎖
- 自旋鎖原理非常簡單,如果持有鎖的線程能在很短時間內釋放鎖資源,那么那些等待競爭鎖的線程就不需要做內核態(tài)和用戶態(tài)之間的切換進入阻塞掛起狀態(tài),它們只需要等一等(自旋),等持有鎖的線程釋放鎖后即可立即獲取鎖,這樣就避免用戶線程和內核的切換的消耗。
- 但是線程自旋是需要消耗cup的,說白了就是讓cup在做無用功,如果一直獲取不到鎖,那線程也不能一直占用cup自旋做無用功,所以需要設定一個自旋等待的最大時間。
- 如果持有鎖的線程執(zhí)行的時間超過自旋等待的最大時間扔沒有釋放鎖,就會導致其它爭用鎖的線程在最大等待時間內還是獲取不到鎖,這時爭用線程會停止自旋進入阻塞狀態(tài)。
應用場景
- 當鎖的競爭不激烈,并且鎖占用的時間非常短的時候,自旋鎖就可以大幅度提高性能,因為自旋的消耗會小于線程阻塞掛起操作的消耗。但是如果鎖的競爭激烈,或者鎖占用時間長,這時就不適合用自旋鎖。因為這樣會導致大量的線程一直占著CPU資源長時間做無用功。此時線程自旋的消耗是大于線程阻塞掛起操作的消耗。
時間閾值
- 它是自旋鎖的重要一部分,過長和過短都會對整體系統(tǒng)的性能產生負面影響。JVM對于自旋周期的選擇,jdk1.5這個限度是寫死的,在1.6引入了適應性自旋鎖,它是由前一次在同一個鎖上的自旋時間以及鎖的擁有者的狀態(tài)來決定,基本認為一個線程上下文切換的時間是最佳的一個時間,同時JVM還針對當前CPU的負荷情況做了較多的優(yōu)化:
- 如果平均負載小于CPU數(shù)則一直自旋
- 如果有超過(CPU數(shù)/2)個線程正在自旋,則后來線程直接阻塞
- 如果CPU處于節(jié)電模式則停止自旋
- 如果正在自旋的線程發(fā)現(xiàn)了進入臨界區(qū)的線程變化則延遲自旋時間(自旋計數(shù))或進入阻塞
- 自旋時會適當放棄線程優(yōu)先級之間的差異
疑問和猜想
-
在網上又看到一副關于偏向鎖,輕量級鎖,自旋鎖,重量級鎖的流程圖。圖中和上述的內容的細節(jié)有些出入,特別是輕量級鎖膨脹成重量級鎖部分不同有些大。不知道哪個才是正確的,或者都不正確。希望自己以后可以把這篇文章完善,給出正確的答案。圖如下:
鎖分配和膨脹過程.jpg 我覺得上圖中在左邊的灰色框后應該是把對象頭的Mark Word記錄當前線程的ID從原偏向線程ID改成新線程的ID。然后新線程就能獲取到偏向鎖,再訪問臨界區(qū)(代碼塊中的內容)
希望大??梢栽u論告訴我這幾種鎖膨脹的過程,哪個才是正確的,還是有另外的版本。
