在 Java 語言中,使用 Synchronized 是可以實現線程同步的,即加鎖。而且實現的是悲觀鎖,在操做同步資源的時候直接先加鎖。html
加鎖可使一段代碼在同一時間只有一個線程能夠訪問,在增長安全性的同時,犧牲掉的是程序的執行性能,因此爲了在必定程度上減小得到鎖和釋放鎖帶來的性能消耗,在 jdk6 以後便引入了「偏向鎖」和「輕量級鎖」,因此總共有4種鎖狀態,級別由低到高依次爲:無鎖狀態、偏向鎖狀態、輕量級鎖狀態、重量級鎖狀態。這幾個狀態會隨着競爭狀況逐漸升級。java
注意:鎖能夠升級但不能降級。安全
固然了,在談這四種狀態以前,咱們仍是有必要再簡單瞭解下 synchronized 的原理。數據結構
在使用 synchronized 來同步代碼塊的時候,經編譯後,會在代碼塊的起始位置插入 monitorenter指令,在結束或異常處插入 **monitorexit指令。**當執行到 monitorenter 指令時,將會嘗試獲取對象所對應的 **monitor **的全部權,即嘗試得到對象的鎖。而 synchronized 用的鎖是存放在 Java對象頭 中的。工具
因此引出了兩個關鍵詞:「Java 對象頭」 和 「Monitor」。性能
咱們以 Hotspot 虛擬機爲例,Hotspot 的對象頭主要包括兩部分數據:Mark Word(標記字段)、Klass Pointer(類型指針)。spa
Mark Word:默認存儲對象的 HashCode,分代年齡和鎖標誌位信息。這些信息都是與對象自身定義無關的數據,因此 Mark Word 被設計成一個非固定的數據結構以便在極小的空間內存存儲儘可能多的數據。它會根據對象的狀態複用本身的存儲空間,也就是說在運行期間 Mark Word 裏存儲的數據會隨着鎖標誌位的變化而變化。操作系統
Klass Point:對象指向它的類元數據的指針,虛擬機經過這個指針來肯定這個對象是哪一個類的實例。線程
Monitor 能夠理解爲一個同步工具或一種同步機制,一般被描述爲一個對象。每個 Java 對象就有一把看不見的鎖,稱爲內部鎖或者 Monitor 鎖。設計
Monitor 是線程私有的數據結構,每個線程都有一個可用 monitor record 列表,同時還有一個全局的可用列表。每個被鎖住的對象都會和一個 monitor 關聯,同時 monitor 中有一個 Owner 字段存放擁有該鎖的線程的惟一標識,表示該鎖被這個線程佔用。
無鎖是指沒有對資源進行鎖定,全部的線程都能訪問並修改同一個資源,但同時只有一個線程能修改爲功。
無鎖的特色是修改操做會在循環內進行,線程會不斷的嘗試修改共享資源。若是沒有衝突就修改爲功並退出,不然就會繼續循環嘗試。若是有多個線程修改同一個值,一定會有一個線程能修改爲功,而其餘修改失敗的線程會不斷重試直到修改爲功。
偏向鎖是指當一段同步代碼一直被同一個線程所訪問時,即不存在多個線程的競爭時,那麼該線程在後續訪問時便會自動得到鎖,從而下降獲取鎖帶來的消耗,即提升性能。
當一個線程訪問同步代碼塊並獲取鎖時,會在 Mark Word 裏存儲鎖偏向的線程 ID。在線程進入和退出同步塊時再也不經過 CAS 操做來加鎖和解鎖,而是檢測 Mark Word 裏是否存儲着指向當前線程的偏向鎖。輕量級鎖的獲取及釋放依賴屢次 CAS 原子指令,而偏向鎖只須要在置換 ThreadID 的時候依賴一次 CAS 原子指令便可。
偏向鎖只有遇到其餘線程嘗試競爭偏向鎖時,持有偏向鎖的線程纔會釋放鎖,線程是不會主動釋放偏向鎖的。
關於偏向鎖的撤銷,須要等待全局安全點,即在某個時間點上沒有字節碼正在執行時,它會先暫停擁有偏向鎖的線程,而後判斷鎖對象是否處於被鎖定狀態。若是線程不處於活動狀態,則將對象頭設置成無鎖狀態,並撤銷偏向鎖,恢復到無鎖(標誌位爲01)或輕量級鎖(標誌位爲00)的狀態。
偏向鎖在 JDK 6 及以後版本的 JVM 裏是默認啓用的。能夠經過 JVM 參數關閉偏向鎖:-XX:-UseBiasedLocking=false,關閉以後程序默認會進入輕量級鎖狀態。
輕量級鎖是指當鎖是偏向鎖的時候,卻被另外的線程所訪問,此時偏向鎖就會升級爲輕量級鎖,其餘線程會經過自旋(關於自旋的介紹見文末)的形式嘗試獲取鎖,線程不會阻塞,從而提升性能。
輕量級鎖的獲取主要由兩種狀況:① 當關閉偏向鎖功能時;② 因爲多個線程競爭偏向鎖致使偏向鎖升級爲輕量級鎖。
在代碼進入同步塊的時候,若是同步對象鎖狀態爲無鎖狀態,虛擬機將首先在當前線程的棧幀中創建一個名爲鎖記錄(Lock Record)的空間,用於存儲鎖對象目前的 Mark Word 的拷貝,而後將對象頭中的 Mark Word 複製到鎖記錄中。
拷貝成功後,虛擬機將使用 CAS 操做嘗試將對象的 Mark Word 更新爲指向 Lock Record 的指針,並將 Lock Record 裏的 owner 指針指向對象的 Mark Word。
若是這個更新動做成功了,那麼這個線程就擁有了該對象的鎖,而且對象 Mark Word 的鎖標誌位設置爲「00」,表示此對象處於輕量級鎖定狀態。
若是輕量級鎖的更新操做失敗了,虛擬機首先會檢查對象的 Mark Word 是否指向當前線程的棧幀,若是是就說明當前線程已經擁有了這個對象的鎖,那就能夠直接進入同步塊繼續執行,不然說明多個線程競爭鎖。
若當前只有一個等待線程,則該線程將經過自旋進行等待。可是當自旋超過必定的次數時,輕量級鎖便會升級爲重量級鎖(鎖膨脹)。
另外,當一個線程已持有鎖,另外一個線程在自旋,而此時又有第三個線程來訪時,輕量級鎖也會升級爲重量級鎖(鎖膨脹)。
重量級鎖是指當有一個線程獲取鎖以後,其他全部等待獲取該鎖的線程都會處於阻塞狀態。
重量級鎖經過對象內部的監視器(monitor)實現,而其中 monitor 的本質是依賴於底層操做系統的 Mutex Lock 實現,操做系統實現線程之間的切換須要從用戶態切換到內核態,切換成本很是高。
簡言之,就是全部的控制權都交給了操做系統,由操做系統來負責線程間的調度和線程的狀態變動。而這樣會出現頻繁地對線程運行狀態的切換,線程的掛起和喚醒,從而消耗大量的系統資源,致使性能低下。
關於自旋,簡言之就是讓線程喝杯咖啡小憩一下,用代碼解釋就是:
do {
// do something
} while (自旋的規則,或者說自旋的次數)
複製代碼
引入自旋這一規則的緣由其實也很簡單,由於阻塞或喚醒一個 Java 線程須要操做系統切換 CPU 狀態來完成,這種狀態轉換須要耗費處理器時間。若是同步代碼塊中的內容過於簡單,狀態轉換消耗的時間有可能比用戶代碼執行的時間還要長。而且在許多場景中,同步資源的鎖定時間很短,爲了這一小段時間去切換線程,這部分操做的開銷實際上是得不償失的。
因此,在物理機器有多個處理器的狀況下,當兩個或以上的線程同時並行執行時,咱們就可讓後面那個請求鎖的線程不放棄 CPU 的執行時間,看看持有鎖的線程是否很快就會釋放鎖。而爲了讓當前線程「稍等一下」,咱們需讓當前線程進行自旋。若是在自旋完成後前面鎖定同步資源的線程已經釋放了鎖,那麼當前線程就能夠沒必要阻塞而是直接獲取同步資源,從而避免切換線程的開銷。
自旋鎖自己是有缺點的,它不能代替阻塞。自旋等待雖然避免了線程切換的開銷,但它要佔用處理器時間。若是鎖被佔用的時間很短,自旋等待的效果就會很是好。反之,若是鎖被佔用的時間很長,那麼自旋的線程只會白浪費處理器資源。
因此,自旋等待的時間必需要有必定的限度,若是自旋超過了限定次數(默認是10次,可使用 -XX:PreBlockSpin 來更改)沒有成功得到鎖,就應當掛起線程。
自旋鎖在 JDK1.4.2 中引入,使用 -XX:+UseSpinning 來開啓。JDK 6 中變爲默認開啓,而且引入了自適應的自旋鎖(適應性自旋鎖)。
自適應自旋鎖意味着自旋的時間(次數)再也不固定,而是由前一次在同一個鎖上的自旋時間及鎖的擁有者的狀態來決定。若是在同一個鎖對象上,自旋等待剛剛成功得到過鎖,而且持有鎖的線程正在運行中,那麼虛擬機就會認爲此次自旋也是頗有可能再次成功,進而它將容許自旋等待持續相對更長的時間。若是對於某個鎖,自旋不多成功得到過,那在之後嘗試獲取這個鎖時將可能省略掉自旋過程,直接阻塞線程,避免浪費處理器資源。
偏向鎖經過對比 Mark Word 解決加鎖問題,避免執行CAS操做。
輕量級鎖是經過用 CAS 操做和自旋來解決加鎖問題,避免線程阻塞和喚醒而影響性能。
重量級鎖是將除了擁有鎖的線程之外的線程都阻塞。
原文地址:www.jetchen.cn/synchronize…