同一個時間只容許一個線程擁有一個對象鎖,這樣在同一時間只有一個線程對須要同步的代碼塊進行訪問java
必須確保在某個線程的某個對象鎖在釋放以前,對某個共享變量所作的改變,對於下一個擁有在這個對象鎖的線程是可見的,不然另外線程讀取的是本地的副本從而進行操做,致使結果不一致。安全
從互斥鎖的設計上來講,一個線程試圖操做一個由其餘線程持有的臨界資源的時候,這個線程會處於堵塞狀態。數據結構
若是一個線程再次請求本身持有對象鎖的臨界資源的時候,這就屬於重入鎖。多線程
所以在一個線程調用synchronized方法的同時在其方法體內部調用該對象另外一個synchronized方法,也就是說一個線程獲得一個對象鎖後再次請求該對象鎖,是容許的,這就是synchronized的可重入性。性能
鎖對象存儲在Java對象頭裏面測試
位數 | 頭對象結構 | |
---|---|---|
32 | Mark word | 存儲對象的HashCode,GC分代年齡,鎖類型,鎖標記 |
32 | Class MeteDataAddress | 類型指針:指向實例對象所屬的類 |
MarkWord被設定爲一個非固定的數據結構,用來存儲更多的數據,結構以下(這裏不是很懂)操作系統
Monitor(內部鎖,Monitor鎖,管程,監視器鎖,也就是和對象鎖對應的對象).net
每一個對象都存在這一個Monitor與之關聯線程
每一個Java對象天生帶有這把看不見的鎖,在MarkWord的結構中,重量級鎖的標記爲是10,也就是指針就是指向Monitor對象的起始地址,在這裏也就說明了Synchronized的默認鎖是重量級鎖。monitor能夠與對象一塊兒建立銷燬或當線程試圖獲取對象鎖時自動生成,但當一個 monitor 被某個線程持有後,它便處於鎖定狀態。設計
在Java虛擬機中,Monitor是有MonitorObject所實現的,部分結構以下
_owner:指向持有ObjectMonitor對象的線程
_WaitSet:存放處於wait狀態的線程隊列
_EntryList:存放處於等待鎖block狀態的線程隊列
_count:用來記錄該線程獲取鎖的次數
ObjectMonitor中有兩個隊列,_WaitSet 和 _EntryList,用來保存ObjectWaiter對象列表( 每一個等待鎖的線程都會被封裝成ObjectWaiter對象),_owner指向持有ObjectMonitor對象的線程,當有多個線程訪問同一塊同步代碼塊的時候,線程會線程會進入_EntryList,當線程獲取到對象的monitor 後進入 _Owner 區域並把monitor中的owner變量設置爲當前線程,同時monitor中的計數器count加1,若線程調用 wait() 方法,將釋放當前持有的monitor,owner變量恢復爲null,count自減1,同時該線程進入 WaitSet集合中等待被喚醒。若當前線程執行完畢也將釋放monitor(鎖)並復位變量的值,以便其餘線程進入獲取monitor(鎖)。
Monitorenter和Monitorexit
Synchronized代碼塊執行原理
字節碼中可知同步語句塊的實現使用的是monitorenter 和 monitorexit 指令,其中monitorenter指令指向同步代碼塊的開始位置,monitorexit指令則指明同步代碼塊的結束位置 。當執行monitorenter指令時,若是當前線程獲取對象鎖所對應的monitor的特權的時候
1 會去檢查monitor的對象的count是否爲0
2 若是爲0的話就獲取成功,而且將count置爲1
3 假若其餘線程已經擁有 objectref 的 monitor 的全部權,那當前線程將被阻塞,直到正在執行線程執行完畢,即monitorexit指令被執行,執行線程將釋放 monitor(鎖)並設置計數器值爲0 ,其餘線程將有機會持有 monitor 。
編譯器將會確保不管方法經過何種方式完成,方法中調用過的每條 monitorenter 指令都有執行其對應 monitorexit 指令,而不管這個方法是正常結束仍是異常結束。爲了保證在方法異常完成時 monitorenter 和 monitorexit 指令依然能夠正確配對執行,編譯器會自動產生一個異常處理器,這個異常處理器聲明可處理全部的異常,它的目的就是用來執行 monitorexit 指令。通常字節碼文件中都會多出一條monitorexit指令。
Synchronized方法執行原理
方法級的同步是隱式,即無需經過字節碼指令來控制的,它實如今方法調用和返回操做之中。JVM能夠從ACC_SYNCHRONIZED 訪問標誌區分一個方法是否同步方法。當方法調用時,調用指令將會檢查方法的 ACC_SYNCHRONIZED 訪問標誌是否被設置,若是設置了,執行線程將先持有monitor,而後再執行方法,最後再方法完成(不管是正常完成仍是非正常完成)時釋放monitor。
若是一個同步方法執行期間拋出了異常,而且在方法內部沒法處理此異常,那這個同步方法所持有的monitor將在異常拋到同步方法以外時自動釋放
自旋鎖
synchronized在jdk1.6以前的鎖是重量級鎖,對於互斥同步的性能來講,阻塞掛起的是影響最大的。由於掛起線程和恢復線程都是要讓操做系統從用戶態轉化到內核態中完成,而這兩個狀態的轉換是比較影響性能的。
大多數狀況下,線程擁有鎖的時間不會太長,若是直接掛起的話,會影響系統的性能。由於前面說過,線程切換是須要在操做系統的用戶態和內核態之間轉換的。因此爲了解決這個問題,引進了自旋鎖。
自旋鎖假設在不久,當前線程能夠得到這個鎖,所以JVM就讓這個想要得到鎖的線程,先作幾個空循環先,讓這個線程先不要放棄佔有CPU資源的機會,通過若干次空循環以後,若是得到鎖,那麼就順利的進入臨界區。不然,你也不能讓這個線程一直佔有CPU資源呀,因此通過大概10次空循環以後,就只能老老實實地掛起了。
自旋適應鎖
自旋適應鎖就是從自旋鎖改進而來的。在自旋鎖的基礎上,假如A線程經過自旋必定的時間以後得到了鎖,而後釋放鎖。這時B線程也得到了這個鎖,若是此時A線程再次想獲得這個鎖,那麼JVM就會根據以前A線程曾經得到過這個鎖,那麼我就給你適當地增長一點空循環的次數,好比說從10次空循環到100次。假若有個C線程,他也想得到這個鎖,也得自旋等待,但是不多輪到他或者沒獲得過這個鎖(多是被A搶了機會或者其餘的),那麼JVM就會認爲C線程之後可能沒什麼機會得到了,就適當地減小C線程的空循壞次數甚至不讓他作空循環。
偏向鎖
若是A線程第一次得到鎖,那麼鎖就進入偏向模式(虛擬機把對象頭中的標誌位設爲「01」),MarkWord的結構也變成偏向鎖結構,若是沒有其餘線程和A線程競爭,A線程再次請求該鎖時,無需任何同步操做
只須要檢查MarkWord的鎖標記位是否爲偏向鎖和當前線程的Id是否爲ThreadId便可。
也就是說當一個線程訪問同步塊而且獲取鎖的時候,會經過CAS操做在對象頭的偏向鎖結構裏記錄線程的ID,若是記錄成功,線程在進入和退出同步塊時,不須要進行CAS操做來加鎖和解鎖,從而提升程序的性能。
TIPS:偏向鎖只能被第一個獲取它的線程進行 CAS 操做,一旦出現線程競爭鎖對象,其它線程不管什麼時候進行 CAS 操做都會失敗。
加鎖具體步驟以下
先檢查Mark Word是否爲可偏向狀態,也就是說是否 是偏向鎖1,鎖標識位爲01
若是是可偏向狀態,那麼就測試Mark Word結構的線程ID是否是和當前線程的ID一致,
若是是就直接執行同步代碼塊。
若是不是就經過CAS操做競爭鎖,
若是操做成功,就把Mark Word的線程ID設置爲線程的ID
若是操做失敗,那麼就說明此時有多線程競爭的狀態,等到安全點,得到偏向鎖的線程就掛起,進行解鎖操做。偏向鎖升級爲輕量鎖,被阻塞在安全點的線程繼續往下執行同步代碼塊。
解鎖
當得到偏向鎖的線程掛起以後,就會進行解鎖操做。
在解鎖成功以後,JVM判斷此時線程的狀態,
若是尚未執行完同步代碼,則直接將偏向鎖升級爲輕量級鎖,而後繼續執行剩下的代碼塊。
若是此時已經執行完同步代碼,則撤銷鎖爲無鎖狀態,之後執行同步代碼的時候JVM則會直接升級爲輕量鎖。
輕量鎖(加鎖解鎖操做是須要依賴屢次CAS原子指令的)
偏向鎖一旦受到多線程競爭,就會膨脹爲輕量鎖
獲取鎖
釋放鎖
重量級鎖
重量級鎖經過對象內部的監視器(monitor)實現
其中monitor的本質是依賴於底層操做系統的Mutex Lock實現
操做系統實現線程之間的切換須要從用戶態到內核態的切換,切換成本很是高。
鎖主要存在四種狀態,無狀態鎖,偏向鎖,輕量鎖,重量鎖,會隨着線程競爭的程度逐漸增大。鎖只能夠單向升級,不能夠降級。
主要是爲了提升得到鎖和解鎖的效率。
鎖類型 | 特徵 | 優勢 | 缺點 | 使用場景 |
---|---|---|---|---|
偏向鎖 | 只須要比較ThreadId | 加鎖和解鎖不須要額外的消耗,和執行非同步代碼塊時間相差無幾 | 若是線程之間有競爭,會增長鎖撤銷的消耗 | 當程序大部分只有一個線程操做的時候 |
輕量鎖 | 自旋 | 競爭線程不會阻塞,提升了程序的響應速度 | 始終得不到鎖的線程使用自旋會消耗CPU | 追求響應時間,同步執行代碼比較快的時候 |
重量鎖 | 依賴Mutex(操做系統的互斥) | 線程競爭不使用自旋,不怎麼會消耗CPU | 線程阻塞,響應緩慢 | 同步代碼執行比較慢的狀況 |
這裏有一張原理圖(盜用別人的圖),把上述的文字都進行了一個總結