轉載自併發編程網 – ifeve.com本文連接地址: 聊聊併發(二)Java SE1.6中的Synchronizedjava
①: 對於普通方法,鎖是當前實例對象;編程
②:對於靜態同步方法,鎖是class對象。數組
③:同步方法塊,鎖是synchronized後面括號裏的對象。安全
當一個線程試圖訪問同步代碼塊時,它首先必須獲得鎖,退出或拋出異常時必須釋放鎖。多線程
那麼鎖存在哪裏呢?併發
鎖裏面會存儲什麼信息呢?性能
JVM規範規定JVM基於進入和退出Monitor對象來實現方法同步和代碼塊同步,但二者的實現細節不同。測試
代碼塊同步是使用monitorenter和monitorexit指令實現,而方法同步是使用另一種方式實現的,細節在JVM規範裏並無詳細說明,可是方法的同步一樣可使用這兩個指令來實現。優化
① monitorenter指令是在編譯後插入到同步代碼塊的開始位置,而monitorexit是插入到方法結束處和異常處。spa
② JVM要保證每一個monitorenter必須有對應的monitorexit與之配對。
③ 任何對象都有一個 monitor 與之關聯,當且一個monitor 被持有後,它將處於鎖定狀態。
④ 線程執行到 monitorenter 指令時,將會嘗試獲取對象所對應的 monitor 的全部權,即嘗試得到對象的鎖。
鎖存在Java對象頭裏。若是對象是數組類型,則虛擬機用3個Word(字寬)存儲對象頭,若是對象是非數組類型,則用2字寬存儲對象頭。
在32位虛擬機中,一字寬等於四字節,即32bit。
長度 | 內容 | 說明 |
32/64bit | Mark Word | 存儲對象的hashCode或鎖信息等。 |
32/64bit | Class Metadata Address | 存儲到對象類型數據的指針 |
32/64bit | Array length | 數組的長度(若是當前對象是數組) |
Java對象頭裏的Mark Word裏默認存儲對象的HashCode,分代年齡和鎖標記位。32位JVM的Mark Word的默認存儲結構以下:
25 bit | 4bit | 1bit是不是偏向鎖 | 2bit鎖標誌位 | |
無鎖狀態 | 對象的hashCode | 對象分代年齡 | 0 | 01 |
在運行期間Mark Word裏存儲的數據會隨着鎖標誌位的變化而變化。Mark Word可能變化爲存儲如下4種數據:
鎖狀態 | 25 bit |
4bit |
1bit | 2bit | ||
23bit | 2bit | 是不是偏向鎖 | 鎖標誌位 | |||
輕量級鎖 | 指向棧中鎖記錄的指針 | 00 | ||||
重量級鎖 | 指向互斥量(重量級鎖)的指針 | 10 | ||||
GC標記 | 空 | 11 | ||||
偏向鎖 | 線程ID | Epoch | 對象分代年齡 | 1 | 01 |
在64位虛擬機下,Mark Word是64bit大小的,其存儲結構以下:
鎖狀態 | 25bit |
31bit |
1bit |
4bit |
1bit | 2bit | |
cms_free | 分代年齡 | 偏向鎖 | 鎖標誌位 | ||||
無鎖 | unused | hashCode | 0 | 01 | |||
偏向鎖 | ThreadID(54bit) Epoch(2bit) | 1 | 01 |
Java SE1.6爲了減小得到鎖和釋放鎖所帶來的性能消耗,引入了「偏向鎖」和「輕量級鎖」,
因此在Java SE1.6裏鎖一共有四種狀態,無鎖狀態,偏向鎖狀態,輕量級鎖狀態和重量級鎖狀態,它會隨着競爭狀況逐漸升級。
鎖能夠升級但不能降級,意味着偏向鎖升級成輕量級鎖後不能降級成偏向鎖。
這種鎖升級卻不能降級的策略,目的是爲了提升得到鎖和釋放鎖的效率,下文會詳細分析。
Hotspot的做者通過以往的研究發現大多數狀況下鎖不只不存在多線程競爭,並且老是由同一線程屢次得到,爲了讓線程得到鎖的代價更低而引入了偏向鎖。
1)當一個線程訪問同步塊並獲取鎖時,會在對象頭和棧幀中的鎖記錄裏存儲鎖偏向的線程ID,
2)之後該線程在進入和退出同步塊時不須要花費CAS操做來加鎖和解鎖,而只需簡單的測試一下對象頭的Mark Word裏是否存儲着指向當前線程的偏向鎖,
3.1)若是測試成功,表示線程已經得到了鎖;
3.2)若是測試失敗,則須要再測試下Mark Word中偏向鎖的標識是否設置成1(表示當前是偏向鎖),
3.2.1) 若是沒有設置,則使用CAS競爭鎖;
3.2.2) 若是設置了,則嘗試使用CAS將對象頭的偏向鎖指向當前線程。
偏向鎖的撤銷:偏向鎖使用了一種等到競爭出現才釋放鎖的機制,因此當其餘線程嘗試競爭偏向鎖時,持有偏向鎖的線程纔會釋放鎖。
偏向鎖的撤銷,須要等待全局安全點(在這個時間點上沒有字節碼正在執行),
它會首先暫停擁有偏向鎖的線程,而後檢查持有偏向鎖的線程是否活着,
若是線程不處於活動狀態,則將對象頭設置成無鎖狀態,
若是線程仍然活着,擁有偏向鎖的棧會被執行,遍歷偏向對象的鎖記錄,
棧中的鎖記錄和對象頭的Mark Word要麼從新偏向於其餘線程,
要麼恢復到無鎖或者標記對象不適合做爲偏向鎖,最後喚醒暫停的線程。
下圖中的線程1演示了偏向鎖初始化的流程,線程2演示了偏向鎖撤銷的流程。
關閉偏向鎖:偏向鎖在Java 6和Java 7裏是默認啓用的,可是它在應用程序啓動幾秒鐘以後才激活,若有必要可使用JVM參數來關閉延遲-XX:BiasedLockingStartupDelay = 0。若是你肯定本身應用程序裏全部的鎖一般狀況下處於競爭狀態,能夠經過JVM參數關閉偏向鎖-XX:-UseBiasedLocking=false,那麼默認會進入輕量級鎖狀態。
輕量級鎖加鎖:
線程在執行同步塊以前,JVM會先在當前線程的棧楨中建立用於存儲鎖記錄的空間,並將對象頭中的Mark Word複製到鎖記錄中,官方稱爲Displaced Mark Word。
而後線程嘗試使用CAS將對象頭中的Mark Word替換爲指向鎖記錄的指針。
若是成功,當前線程得到鎖,
若是失敗,表示其餘線程競爭鎖,當前線程便嘗試使用自旋來獲取鎖。
輕量級鎖解鎖:
輕量級解鎖時,會使用原子的CAS操做來將Displaced Mark Word替換回到對象頭,
若是成功,則表示沒有競爭發生。
若是失敗,表示當前鎖存在競爭,鎖就會膨脹成重量級鎖。下圖是兩個線程同時爭奪鎖,致使鎖膨脹的流程圖。
由於自旋會消耗CPU,爲了不無用的自旋(好比得到鎖的線程被阻塞住了),一旦鎖升級成重量級鎖,就不會再恢復到輕量級鎖狀態。
當鎖處於這個狀態下,其餘線程試圖獲取鎖時,都會被阻塞住,當持有鎖的線程釋放鎖以後會喚醒這些線程,被喚醒的線程就會進行新一輪的奪鎖之爭。
鎖 |
優勢 |
缺點 |
適用場景 |
偏向鎖 |
加鎖和解鎖不須要額外的消耗,和執行非同步方法比僅存在納秒級的差距。 |
若是線程間存在鎖競爭,會帶來額外的鎖撤銷的消耗。 |
適用於只有一個線程訪問同步塊場景。 |
輕量級鎖 |
競爭的線程不會阻塞,提升了程序的響應速度。 |
若是始終得不到鎖競爭的線程使用自旋會消耗CPU。 |
追求響應時間。 同步塊執行速度很是快。 |
重量級鎖 |
線程競爭不使用自旋,不會消耗CPU。 |
線程阻塞,響應時間緩慢。 |
追求吞吐量。 同步塊執行速度較長。 |
TODO
ConcurrentHashMap.java
BlockingQueue.java
本文一些內容參考了Hotspot源碼 。對象頭源碼markOop.hpp。偏向鎖源碼biasedLocking.cpp。以及其餘源碼ObjectMonitor.cpp和BasicLock.cpp。