全文使用synchronized來說明。
synchronized給對象上鎖,先上偏向鎖,在上輕量級鎖,最後上重量級鎖。上什麼鎖,是gvm根據競爭程度自行變換的。
重量級鎖
計算機操作系統本有Monitor對象,稱爲管程。在java裏面看不到此對象。
每個Java對象都可以關聯一個monitor對象,如果使用了synchronized給對象上重量級鎖後,該對象的Mark word就被設置指向monitor對象的指針。
Monitor對象結構
- WaitSet:是線程等待隊列。狀態爲WAITING
- EntryList:線程阻塞隊列.狀態爲BLOCKED
- Owner:正在執行的線程(可能很多線程競爭一個資源,但只有一個線程能夠成功,此時Owner就置爲此線程)
原理
- 新建的對象,此時Mark Word關聯一個Monitor對象(即Mark Word記錄一個monitor對象的地址)。此時Monitor對象裏面的Owner爲null.
因爲還沒有線程去獲得Monitor鎖。
- 多個線程競爭,只有線程1成功。其他進入阻塞隊列。(或者說,只要owner非空,那麼其他線程就要進入阻塞隊列)
當線程1執行完畢後,通知阻塞隊列裏的線程,引起它們的非公平性競爭。
- 若此時Owner線程調用wait方法,那麼會進入WaitSet。
當被喚醒時(如調用notify())會進入EntryList重新競爭。
輕量級鎖
如果一個對象雖然有多線程訪問,但多線程訪問的時間是錯開的(或者說沒有競爭)的話,可以使用輕量級鎖來優化。
當有競爭時,會發生鎖膨脹,變爲重量級鎖,
java 對象頭
以32爲虛擬機爲例
普通對象是:
- Klass Word:是一個指針,通過他可以知道是個啥類對象
- Mark Word:
如 Normal ,即沒有上鎖,末尾兩位是01;輕量級鎖是00
下面用Hashcode age Bias 01 代替最初的Mark Word
原理
每個線程的棧幀都會包含一個鎖記錄的結構,內部可以保存鎖對象的Mark Word
- 當線程執行到臨界區代碼時,對obj上鎖。讓鎖記錄中的Object reference 指向鎖對象,並嘗試用cas替換Object的Mark Word,將 Mark Word的值存入鎖記錄。
-
cas替換成功:那麼Object對象就會存儲鎖記錄狀態00和地址 ,表示由該線程給對象加鎖。 當Mark Word末尾是01時,纔可以替換成功
-
cas替換失敗
- case 1:其他線程已經持有了改Object的輕量級鎖,表明有競爭,進入鎖膨脹過程
- case 2:自己執行synchronized鎖重入,再添加一條Lock Record作爲重入的計數
此時進行cas操作自然會失敗。
最後再講鎖膨脹。
-
解鎖
- 當Lock Record 記錄裏面存在null,說明存在重進入,這時重置鎖記錄,表示重進入計數減一
- 鎖記錄不爲空,這時用cas操作把鎖記錄裏面保存的Mark Word替換給對象頭
- 成功則解鎖成功
- 失敗說明進行了鎖膨脹或已經升級爲重量級鎖,進入重量級鎖的解鎖流程。
-
鎖膨脹
在嘗試加輕量級鎖的過程中,CAS操作無法成功,一個原因就是有其他線程爲此對象加上了輕量級鎖,這時需要進行鎖膨脹,將輕量級鎖變爲重量級鎖。
如線程1持有object的鎖,這時線程2也想要,但進行cas操作失敗,這時候會發生:
- 爲object對象申請Monitor鎖,讓object指向重量級鎖地址(此時後兩位爲10,即重量級鎖)
- 然後線程1進入Monitor的EntryList阻塞
當線程1執行完後,想要退出臨界區,使用cas將Mark Word的值替換給對象頭,但是會失敗。因爲object的Mark Word後兩位變爲10,已經不再是00了。
這時按照Monitor地址找到monitor對象,設置Owner爲null,再喚醒線程2.
偏向鎖
如果只有一個線程,它多次重進入,那會多次建立空的鎖記錄,作爲鎖重入的計數。
因此,java 6中引入了偏向鎖來做進一步優化:只有第一次使用CAS將線程ID設置到對象的Mark Word,之後只要發現這個線程ID是自己的就表示沒有發生競爭,不用重新CAS。以後只要不發生競爭,這個對象就歸該線程所有。
當線程請求到鎖對象後,將鎖對象的狀態標誌位改爲01,即偏向模式。然後使用CAS操作將線程的ID記錄在鎖對象的Mark Word中。以後該線程可以直接進入同步塊,連CAS操作都不需要。但是,一旦有第二條線程需要競爭鎖,那麼偏向模式立即結束,進入輕量級鎖的狀態。