synchronized
Java SE 1.6前重量級鎖,Java SE 1.6 中爲了減小得到鎖和釋放鎖帶來的性能消耗而引入的偏向鎖和輕量級鎖。java
synchronized 的基本語法
synchronized 有三種方式來加鎖,分別是安全
- 修飾實例方法,做用於當前實例加鎖,進入同步代碼前要得到當前實例的鎖
- 靜態方法,做用於當前類對象加鎖,進入同步代碼前要得到當前類對象的鎖
- 修飾代碼塊,指定加鎖對象,對給定對象加鎖,進入同步代碼庫前要得到給定對象的鎖。 不一樣的修飾類型,表明鎖的控制粒度
synchronized的存儲
Mark word 記錄了對象和鎖有關的信息,當某個對象被synchronized 關鍵字當成同步鎖時,那麼圍繞這個鎖的一系列操做都和 Mark word 有關係。Mark Word 在 32 位虛擬機的長度是 32bit、在 64 位虛擬機的長度是 64bit。線程在獲取鎖的時候,實際上就是得到一個監視器對象(monitor) ,monitor 能夠認爲是一個同步對象,全部的Java對象都攜帶 monitor,多個線程訪問同步代碼塊時,至關於去爭搶對象監視器修改對象中的鎖標識。jvm
synchronized 鎖的升級
偏向鎖
當一個線程訪問加了同步鎖的代碼塊時,會在對象頭中存儲當前線程的ID,後續這個線程進入和退出這段加了同步鎖的代碼塊時,不須要再次加鎖和釋放鎖。而是直接比較對象頭裏面是否存儲了指向當前線程的偏向鎖。若是相等表示偏向鎖是偏向於當前線程的,就不須要再嘗試得到鎖了。性能
偏向鎖的獲取和撤銷邏輯
- 首先獲取鎖對象的 Markword,判斷是否處於可偏向狀態。(biased_lock=一、且 ThreadId 爲空)
- 若是是可偏向狀態,則經過 CAS 操做,把當前線程的 ID寫入到 MarkWord
- 若是 cas 成功,那麼 markword會記錄ThreadId,同時置偏向標誌位1。表示已經得到了鎖對象的偏向鎖,接着執行同步代碼塊
- 若是 cas 失敗,說明有其餘線程已經得到了偏向鎖,這種狀況說明當前鎖存在競爭,須要撤銷已得到偏向鎖的線程,而且把它持有的鎖升級爲輕量級鎖(這個 操做須要等到全局安全點,也就是沒有線程在執行字節碼)才能執行
- 若是是已偏向狀態,須要檢查 markword 中存儲的ThreadID 是否等於當前線程的 ThreadID
- 若是相等,不須要再次得到鎖,可直接執行同步代碼塊
- 若是不相等,說明當前鎖偏向於其餘線程,須要撤銷偏向鎖並升級到輕量級鎖
偏向鎖的撤銷
偏向鎖的撤銷並非把對象恢復到無鎖可偏向狀態(由於偏向鎖並不存在鎖釋放的概念),而是在獲取偏向鎖的過程當中,發現 cas 失敗也就是存在線程競爭時,直接把被偏向的鎖對象升級到被加了輕量級鎖的狀態。對原持有偏向鎖的線程進行撤銷時,原得到偏向鎖的線程有兩種狀況:操作系統
- 原得到偏向鎖的線程若是已經退出了臨界區,也就是同步代碼塊執行完了,那麼這個時候會把對象頭設置成無鎖狀態而且爭搶鎖的線程能夠基於 CAS 從新偏向當前線程
- 若是原得到偏向鎖的線程的同步代碼塊還沒執行完,處於臨界區以內,這個時候會把原得到偏向鎖的線程升級爲輕量級鎖後繼續執行同步代碼塊
在咱們的應用開發中,絕大部分狀況下必定會存在 2 個以上的線程競爭,那麼若是開啓偏向鎖,反而會提高獲取鎖的資源消耗。因此能夠經過 jvm 參數 UseBiasedLocking 來設置開啓或關閉偏向鎖。
輕量級鎖
輕量級鎖的基本原理
輕量級鎖的加鎖和解鎖邏輯
鎖升級爲輕量級鎖以後,對象的Markword也會進行相應的的變化。 升級爲輕量級鎖的過程:線程
- 線程在本身的棧楨中建立鎖記錄LockRecord。
- 將鎖對象的對象頭中的MarkWord複製到線程的剛剛建立的鎖記錄中。
- 將鎖記錄中的Owner指針指向鎖對象。
- 將鎖對象的對象頭的MarkWord替換爲指向鎖記錄的指針。
自旋鎖
輕量級鎖在加鎖過程當中,用到了自旋鎖
所謂自旋,就是指當有另一個線程來競爭鎖時,這個線程會在原地循環等待,而不是把該線程給阻塞,直到那個得到鎖的線程釋放鎖以後,這個線程就能夠立刻得到鎖的。注意,鎖在原地循環的時候,是會消耗 cpu 的,就至關於在執行一個啥也沒有的for循環。因此,輕量級鎖適用於那些同步代碼塊執行的很快的場景,這樣,線程原地等待很短的時間就可以得到鎖了。
自旋鎖的使用,其實也是有必定的機率背景,在大部分同步代碼塊執行的時間都是很短的。因此經過看似無異議的循環反而能提高鎖的性能。可是自旋必需要有必定的條件控制,不然若是一個線程執行同步代碼塊的時間很長,那麼這個線程不斷的循環反而會消耗 CPU 資源。默認狀況下自旋的次數是 10 次,能夠經過 preBlockSpin 來修改。
在 JDK1.6 以後,引入了自適應自旋鎖,自適應意味着自旋的次數不是固定不變的,而是根據前一次在同一個鎖上自旋的時間以及鎖的擁有者的狀態來決定。 若是在同一個鎖對象上,自旋等待剛剛成功得到過鎖,而且持有鎖的線程正在運行中,那麼虛擬機就會認爲此次自旋也是頗有可能再次成功,進而它將容許自旋等待持續相對更長的時間。若是對於某個鎖,自旋不多成功得到過,那在之後嘗試獲取這個鎖時將可能省略掉自旋過程,直接阻塞線程,避免浪費處理器資源。指針
輕量級鎖的解鎖
輕量級鎖的鎖釋放邏輯其實就是得到鎖的逆向邏輯,經過CAS操做把線程棧幀中的LockRecord替換回到鎖對象的MarkWord中,若是成功表示沒有競爭。若是失敗,表示當前鎖存在競爭,那麼輕量級鎖就會膨脹成爲重量級鎖。
對象
重量級鎖
當輕量級鎖膨脹到重量級鎖以後,意味着線程只能被掛起阻塞來等待被喚醒了。 每個 JAVA 對象都會與一個監視器 monitor 關聯,咱們能夠把它理解成爲一把鎖,當一個線程想要執行一段被synchronized 修飾的同步方法或者代碼塊時,該線程得先獲取到 synchronized 修飾的對象對應的 monitor。
monitorenter 表示去得到一個對象監視器。 monitorexit 表示釋放 monitor 監視器的全部權,使得其餘被阻塞的線程能夠嘗試去得到這個監視器。 monitor 依賴操做系統的MutexLock(互斥鎖)來實現的, 線程被阻塞後便進入內核(Linux)調度狀態,這個會致使系統在用戶態與內核態之間來回切換,嚴重影響鎖的性能。
任意線程對 Object(Object 由 synchronized 保護)的訪問,首先要得到 Object 的監視器。若是獲取失敗,線程進入同步隊列,線程狀態變爲 BLOCKED。當訪問 Object 的前驅(得到了鎖的線程)釋放了鎖,則該釋放操做喚醒阻塞在同步隊列中的線程,使其從新嘗試對監視器的獲取。blog
總結
偏向鎖
此時當 Thread#1 進入臨界區時,JVM 會將 lockObject 的對象頭 Mark Word 的鎖標誌位設爲「01」,同時會用 CAS 操做把 Thread#1 的線程 ID 記錄到 Mark Word 中,此時進入偏向模式。所謂「偏向」,指的是這個鎖會偏向於 Thread#1,若接下來沒有其餘線程進入臨界區,則 Thread#1 再出入臨界區無需再執行任何同步操做。也就是說,若只有Thread#1 會進入臨界區,實際上只有 Thread#1 初次進入臨界區時須要執行 CAS 操做,之後再出入臨界區都不會有同步操做帶來的開銷。
輕量級鎖
偏向鎖的場景太過於理想化,更多的時候是 Thread#2 也會嘗試進入臨界區, 若是 Thread#2 也進入臨界區可是Thread#1 尚未執行完同步代碼塊時,會暫停 Thread#1而且升級到輕量級鎖。Thread#2 經過自旋再次嘗試以輕量級鎖的方式來獲取鎖
重量級鎖
若是 Thread#1 和 Thread#2 正常交替執行,那麼輕量級鎖基本可以知足鎖的需求。可是若是 Thread#1 和 Thread#2同時進入臨界區,那麼輕量級鎖就會膨脹爲重量級鎖,意味着 Thread#1 線程得到了重量級鎖的狀況下,Thread#2就會被阻塞。隊列
Synchronized 結合 Java Object 對象中的wait,notify,notifyAll
wait/notify/notifyall 基本概念
wait:表示持有對象鎖的線程 A 準備釋放對象鎖權限,釋放 cpu 資源並進入等待狀態。
notify:表示持有對象鎖的線程A準備釋放對象鎖權限,通知 jvm喚醒某個競爭該對象鎖的線程X 。線 程A synchronized 代碼執行結束而且釋放了鎖以後,線程X 直接得到對象鎖權限,其餘競爭線程繼續等待(即便線程X同步完畢,釋放對象鎖,其餘競爭線程仍然等待,直至有新的notify,notifyAll被調用)。 notifyAll:notifyall 和 notify 的區別在於,notifyAll會喚醒全部競爭同一個對象鎖的全部線程,當已經得到鎖的線程A釋放鎖以後,全部被喚醒的線程都有可能得到對象鎖權限 須要注意的是:三個方法都必須在 synchronized 同步關鍵字所限定的做用域中調用,不然會報錯java.lang.IllegalMonitorStateException ,意思是由於沒有同步,因此線程對對象鎖的狀態是不肯定的,不能調用這些方法。另外,經過同步機制來確保線程從wait方法返回時可以感知到notify線程對變量作出的修改。