深刻了解Synchronized原理

Synchronized

互斥性

同一個時間只容許一個線程擁有一個對象鎖,這樣在同一時間只有一個線程對須要同步的代碼塊進行訪問java

可見性

必須確保在某個線程的某個對象鎖在釋放以前,對某個共享變量所作的改變,對於下一個擁有在這個對象鎖的線程是可見的,不然另外線程讀取的是本地的副本從而進行操做,致使結果不一致。安全

重入性

從互斥鎖的設計上來講,一個線程試圖操做一個由其餘線程持有的臨界資源的時候,這個線程會處於堵塞狀態。數據結構

若是一個線程再次請求本身持有對象鎖的臨界資源的時候,這就屬於重入鎖。多線程

所以在一個線程調用synchronized方法的同時在其方法體內部調用該對象另外一個synchronized方法,也就是說一個線程獲得一個對象鎖後再次請求該對象鎖,是容許的,這就是synchronized的可重入性。性能

獲取對象鎖的方式

獲取對象鎖的方式

  1. 修飾實例方法,做用於當前實例加鎖,進行同步代碼塊以前須要得到當前實例的鎖(Synchronized method)
  2. 修飾代碼塊,指定加鎖對象,做用於給定對象加鎖,進入同步代碼快以前要得到給定對象的鎖(Synchronized instance)
  3. 修飾靜態方法,做用於當前類對象加鎖,進入同步代碼塊以前要得到當前類對象的鎖(Synchronized static method)
  4. 修飾類對象,做用於類對象加鎖,進入同步代碼塊以前要得到指定類對象的鎖(Synchronized **.class)

對象鎖和類鎖的區別

  1. 一個線程能夠訪問對象的同步代碼塊時,另一個線程也能夠訪問同一個對象的非同步代碼塊
  2. 若鎖住的是同一個對象,其餘線程訪問對象的同步代碼塊或者同步方法的時候會被阻塞
  3. 同一個類的不一樣對象的對象鎖互不干擾
  4. 類鎖是一種特殊的鎖,由於類就是Class的實例,因此只要不一樣對象都是屬於同一個類,那麼他們的類鎖都是同樣的
  5. 類鎖和對象鎖互不干擾

底層原理

鎖對象存儲在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代碼塊執行原理

字節碼中可知同步語句塊的實現使用的是monitorentermonitorexit 指令,其中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 操做都會失敗。

加鎖具體步驟以下

  1. 先檢查Mark Word是否爲可偏向狀態,也就是說是否 是偏向鎖1,鎖標識位爲01

  2. 若是是可偏向狀態,那麼就測試Mark Word結構的線程ID是否是和當前線程的ID一致,

    若是是就直接執行同步代碼塊。

    若是不是就經過CAS操做競爭鎖,

    ​ 若是操做成功,就把Mark Word的線程ID設置爲線程的ID

    ​ 若是操做失敗,那麼就說明此時有多線程競爭的狀態,等到安全點,得到偏向鎖的線程就掛起,進行解鎖操做。偏向鎖升級爲輕量鎖,被阻塞在安全點的線程繼續往下執行同步代碼塊。

解鎖

當得到偏向鎖的線程掛起以後,就會進行解鎖操做。

在解鎖成功以後,JVM判斷此時線程的狀態,

若是尚未執行完同步代碼,則直接將偏向鎖升級爲輕量級鎖,而後繼續執行剩下的代碼塊。

若是此時已經執行完同步代碼,則撤銷鎖爲無鎖狀態,之後執行同步代碼的時候JVM則會直接升級爲輕量鎖。

輕量鎖(加鎖解鎖操做是須要依賴屢次CAS原子指令的)

偏向鎖一旦受到多線程競爭,就會膨脹爲輕量鎖

獲取鎖

  1. 先判斷當前對象是否處於無鎖狀態,若是是,JVM就首先在想要獲取這個鎖的線程的棧幀中創建一個鎖記錄(Lock Record)的空間,其中header部分用來存儲Mark Word的備份,不然執行3。
  2. JVM利用CAS操做嘗試將對象的Mark Word更新爲指向鎖記錄的指針,若是成功,那麼就得到輕量鎖,就將標誌位設置爲00,執行同步代碼塊,不然執行3。
  3. 判斷當前對象的Mark Word是否指向當前想要競爭的線程的鎖記錄,若是是表示則該線程擁有這個輕量鎖,繼續執行同步代碼塊,也就是重入。不然,說明這個輕量鎖已經被其餘線程擁有,那麼這個先進行自旋獲取鎖,若是一直沒有獲得鎖,那麼輕量鎖則要膨脹爲重量鎖(也就是將標記爲設置爲10),鎖標記設置爲10,後面等待的線程則會進入阻塞狀態,若是經過自旋成功獲取了鎖,那麼輕量鎖不會膨脹爲重量鎖。

釋放鎖

  1. 取出線程鎖記錄以前保存的輕量鎖的Mark Word記錄,經過CAS操做將取出的記錄替換當前對象的Mark Word中
  2. 判斷當前對象的Mark Word是否指向當前線程的鎖記錄
  3. 若是1,2都成功,那麼就成功釋放鎖
  4. 若是1失敗,那麼就是以前有過線程對當前對象的鎖競爭過,可是失敗了,由輕量級鎖變爲重量級鎖,致使Mark Word的結夠發生了改變。那麼後面就釋放鎖,喚醒等待的線程,進行新一輪的競爭。

重量級鎖

重量級鎖經過對象內部的監視器(monitor)實現

其中monitor的本質是依賴於底層操做系統的Mutex Lock實現

操做系統實現線程之間的切換須要從用戶態到內核態的切換,切換成本很是高。

鎖的升級

鎖主要存在四種狀態,無狀態鎖,偏向鎖,輕量鎖,重量鎖,會隨着線程競爭的程度逐漸增大。鎖只能夠單向升級,不能夠降級。

主要是爲了提升得到鎖和解鎖的效率。

各個狀態鎖的優缺點對比

鎖類型 特徵 優勢 缺點 使用場景
偏向鎖 只須要比較ThreadId 加鎖和解鎖不須要額外的消耗,和執行非同步代碼塊時間相差無幾 若是線程之間有競爭,會增長鎖撤銷的消耗 當程序大部分只有一個線程操做的時候
輕量鎖 自旋 競爭線程不會阻塞,提升了程序的響應速度 始終得不到鎖的線程使用自旋會消耗CPU 追求響應時間,同步執行代碼比較快的時候
重量鎖 依賴Mutex(操做系統的互斥) 線程競爭不使用自旋,不怎麼會消耗CPU 線程阻塞,響應緩慢 同步代碼執行比較慢的狀況

最後

這裏有一張原理圖(盜用別人的圖),把上述的文字都進行了一個總結

參考

juejin.im/entry/58998…

blog.csdn.net/championhen…

blog.csdn.net/javazejian/…

相關文章
相關標籤/搜索