<font color="#EE30A7">實現原理</font>
Synchronized能夠保證一個在多線程運行中,同一時刻只有一個方法或者代碼塊被執行,它還能夠保證共享變量的可見性和原子性java
在Java中每一個對象均可以做爲鎖,這是Synchronized實現同步的基礎。具體的表現爲一下3種形式:程序員
- 普通同步方法,鎖是當前實例對象;
- 靜態同步方法,鎖是當前類的Class對象;
- 同步方法快,鎖是Synchronized括號中配置的對象。
當一個線程試圖訪問同步代碼塊時,它必須先獲取到鎖,當同步代碼塊執行完畢或拋出異常時,必須釋放鎖。那麼它是如何實現這一機制的呢?咱們先來看一個簡單的synchronized的代碼:編程
public class SyncDemo { public synchronized void play() {} public void learn() { synchronized(this) { } } }
利用javap工具查看生成的class文件信息分析Synchronized,下面是部分信息數組
public com.zzw.juc.sync.SyncDemo(); descriptor: ()V flags: ACC_PUBLIC Code: stack=1, locals=1, args_size=1 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return LineNumberTable: line 8: 0 LocalVariableTable: Start Length Slot Name Signature 0 5 0 this Lcom/zzw/juc/sync/SyncDemo; public synchronized void play(); descriptor: ()V flags: ACC_PUBLIC, ACC_SYNCHRONIZED Code: stack=0, locals=1, args_size=1 0: return LineNumberTable: line 10: 0 LocalVariableTable: Start Length Slot Name Signature 0 1 0 this Lcom/zzw/juc/sync/SyncDemo; public void learn(); descriptor: ()V flags: ACC_PUBLIC Code: stack=2, locals=3, args_size=1 0: aload_0 1: dup 2: astore_1 3: monitorenter 4: aload_1 5: monitorexit 6: goto 14 9: astore_2 10: aload_1 11: monitorexit 12: aload_2 13: athrow 14: return Exception table: from to target type 4 6 9 any 9 12 9 any
從上面利用javap工具生成的信息咱們能夠看到同步方法是利用ACC_SYNCHRONIZED這個修飾符來實現的,同步代碼塊是利用monitorenter和monitorexit這2個指令來實現的。安全
- 同步代碼塊:monitorenter指令插入到同步代碼塊的開始位置,monitorexit指令插入到同步代碼塊的結束位置,JVM須要保證每個monitorenter都有一個monitorexit與之相對應。任何對象都有一個monitor與之相關聯,當且一個monitor被持有以後,他將處於鎖定狀態。線程執行到monitorenter指令時,將會嘗試獲取對象所對應的monitor全部權,即嘗試獲取對象的鎖;
- 同步方法:synchronized方法則會被翻譯成普通的方法調用和返回指令如:invokevirtual、areturn指令,在JVM字節碼層面並無任何特別的指令來實現被synchronized修飾的方法,而是在Class文件的方法表中將該方法的access_flags字段中的synchronized標誌位置1,表示該方法是同步方法並使用調用該方法的對象或該方法所屬的Class在JVM的內部對象表示Klass作爲鎖對象
在繼續分析Synchronized以前,咱們須要理解2個很是重要的概念:Java對象頭和Monitor多線程
<font color="#EE30A7">Java對象頭</font>
Synchronized用的鎖是存放在Java對象頭裏面的。那麼什麼是對象頭呢?在Hotspot虛擬機中,對象頭包含2個部分:標記字段(Mark Word)和類型指針(Kass point)。 其中Klass Point是是對象指向它的類元數據的指針,虛擬機經過這個指針來肯定這個對象是哪一個類的實例,Mark Word用於存儲對象自身的運行時數據,它是實現輕量級鎖和偏向鎖的關鍵。這裏咱們將重點闡述Mark Word。併發
<font color="#EE30A7">Mark Word</font>
Mark Word用於存儲對象自身的運行時數據,如哈希碼(Hash Code)、GC分代年齡、鎖狀態標誌、線程持有鎖、偏向線程ID、偏向時間戳等,這部分數據在32位和64位虛擬機中分別爲32bit和64bit。一個對象頭通常用2個機器碼存儲(在32位虛擬機中,一個機器碼爲4個字節即32bit),但若是對象是數組類型,則虛擬機用3個機器碼來存儲對象頭,由於JVM虛擬機能夠經過Java對象的元數據信息肯定Java對象的大小,可是沒法從數組的元數據來確認數組的大小,因此用一塊來記錄數組長度。 在32位虛擬機中,Java對象頭的Makr Word的默認存儲結構以下:app
鎖狀態 | 25bit | 4bit | 1bit 是不是偏向鎖 | 2bit鎖標誌位 |
---|---|---|---|---|
無鎖狀態 | 對象的HashCode | 對象分代年齡 | 0 | 01 |
在程序運行期間,對象頭中鎖表標誌位會發生改變。Mark Word可能發生的變化以下: 編程語言
在64位虛擬機中,Java對象頭中Mark Work的長度是64位的,其結構以下:高併發
介紹了Mark Word 下面咱們來介紹下一個重要的機率Monitor。
<font color="#EE30A7">Monitor</font>
Monitor是操做系統提出來的一種高級原語,但其具體的實現模式,不一樣的編程語言都有可能不同。Monitor 有一個重要特色那就是,同一個時刻,只有一個線程能進入到Monitor定義的臨界區中,這使得Monitor可以達到互斥的效果。但僅僅有互斥的做用是不夠的,沒法進入Monitor臨界區的線程,它們應該被阻塞,而且在必要的時候會被喚醒。顯然,monitor 做爲一個同步工具,也應該提供這樣的機制。Monitor的機制以下圖所示:
從上圖中,咱們來分析下Monitor的機制: Mointor能夠看作是一個特殊的房間(這個房間就是咱們在Java線程中定義的臨界區),Monitor在同一時間,保證只能有一個線程進入到這個房間,進入房間即表示持有Monitor,退出房間即表示釋放Monitor。 當一個線程須要訪問臨界區中的數據(即須要獲取到對象的Monitro)時,他首先會在entry-set入口隊列中排隊等待(這裏並非真正的按照排隊順序),若是沒有線程持有對象的Monitor,那麼entry-set隊列中的線程會和waite-set隊列中被喚醒的線程進行競爭,選出一個線程來持有對象Monitor,執行受保護的代碼段,執行完畢後釋放Monitor,若是已經有線程持有對象的Monitor,那麼須要等待其釋放Monitor後再進行競爭。當一個線程擁有對象的Monitor後,這個時候若是調用了Object的wait方法,線程就釋放了Monitor,進入wait-set隊列,當Object的notify方法被執行後,wait-set中的線程就會被喚醒,而後在wait-set隊列中被喚醒的線程和entry-set隊列中的線程一塊兒經過CPU調度來競爭對象的Monitor,最終只有一個線程能獲取對象的Monitor。
須要注意的是: 當一個線程在wait-set中被喚醒後,並不必定會馬上獲取Monitor,它須要和其餘線程去競爭 若是一個線程是從wait-set隊列中喚醒後,獲取到的Monitor,它會去讀取它本身保存的PC計數器中的地址,從它調用wait方法的地方開始執行。
<font color="#EE30A7">鎖的優化和對比</font>
在JavaSE6爲了對鎖進行優化,引入了偏向鎖和輕量級鎖。在JavaSE6中鎖一共有4種狀態,它們從低到高一次是無狀態鎖、偏向鎖、輕量級鎖和重量級鎖。鎖的這幾種狀態會隨着競爭而依次升級,可是鎖是不能降級的。
<font color="#EE30A7">偏向鎖</font>
偏向鎖顧名思義就是偏向於第一個訪問鎖的線程,在運行的過程當中同步鎖只有一個線程訪問,不存在多線程競爭的狀況,則線程不會觸發同步,這種狀況下會給線程加一個偏向鎖。偏向鎖的引入就是爲了讓線程獲取鎖的代價更低。
-
偏向鎖的獲取
(1)訪問Mark Word中偏向鎖的標識是否設置成1,鎖標誌位是否爲01——確認爲可偏向狀態。 (2)若是爲可偏向狀態,則測試線程ID是否指向當前線程,若是是,進入步驟(5),不然進入步驟(3)。 (3)若是線程ID並未指向當前線程,則經過CAS操做競爭鎖。若是競爭成功,則將Mark Word中線程ID設置爲當前線程ID,而後執行(5);若是競爭失敗,執行(4)。 (4)若是CAS獲取偏向鎖失敗,則表示有競爭。當到達全局安全點(safepoint)時得到偏向鎖的線程被掛起,偏向鎖升級爲輕量級鎖,而後被阻塞在安全點的線程繼續往下執行同步代碼。 (5)執行同步代碼。
-
偏向鎖的釋放
偏向鎖的釋放在上面偏向鎖的獲取中的第4步已經提到過。偏向鎖只有在遇到其它線程競爭偏向鎖時,持有偏向鎖的線程纔會釋放。線程是不會主動的去釋放偏向鎖的。偏向鎖的釋放須要等到全局安全點(在這個時間點上沒有正在執行的字節碼),它會首先去暫停擁有偏向鎖的線程,撤銷偏向鎖,設置對象頭中的Mark Word爲無鎖狀態或輕量級鎖狀態,再恢復暫停的線程。
-
偏向鎖的關閉
偏向鎖在Java6和Java7中是默認開啓的,但它是在應用程序啓動幾秒後才激活。若是想消除延時當即開啓,能夠調整JVM參數來關閉延遲:-XX: BiasedLockingStartupDelay=0。若是你肯定應用程序中沒有偏向鎖的存在,你也能夠經過JVM參數關閉偏向鎖: -XX:UseBiasedLocking=false,使用改參數後,程序會默認進入到輕量級鎖狀態。
-
偏向鎖的適用場景
始終只有一個線程在執行同步塊,在它沒有執行完同步代碼塊釋放鎖以前,沒有其它線程去執行同步塊來競爭鎖,在鎖無競爭的狀況下使用。一旦有了競爭就升級爲輕量級鎖,升級爲輕量級鎖的時候須要撤銷偏向鎖,撤銷偏向鎖須要在全局安全點上,這個時候會致使Stop The World,Stop The Wrold 會致使性能降低,所以在高併發的場景下應當禁用偏向鎖。
<font color="#EE30A7">輕量級鎖</font>
輕量級鎖是有偏向鎖競爭升級而來的。引入輕量級鎖的目的是在沒有多線程競爭的狀況下,減小傳統的重量級鎖使用操做系統互斥量產生的性能消耗。
-
輕量級鎖的獲取
(1)在代碼進入同步代碼塊時,若是同步對象沒有被鎖定(鎖標誌位爲「01」狀態),虛擬機首先將在當前線程的棧幀中建了一個名爲鎖記錄(Lock Record)的空間,用於存儲對象目前的Mark Word的拷貝,官方稱之爲 Displaced Mark Word。 (2)虛擬機將使用CAS操做嘗試將對象的Mark Word更新爲指向Lock Record的指針,若是更新成功,則表示獲取到了鎖,並將鎖標誌位設置爲「00」(表示對象處於輕量級鎖狀態)。若是失敗則執行(3)操做。 (3)虛擬機檢查當前對象的Mark Wrod 是否指向當前線程的棧幀,若是是這說明當前線程已經持有了這個對象的鎖,直接進入同步塊繼續運行;不然說明這個鎖對象已經被其它線程持有,這是輕量級鎖就要膨脹爲重量級鎖,鎖標誌的狀態值變動爲「10」,後面等待鎖的線程也要進入阻塞狀態。
-
輕量級鎖的釋放
(1)使用CAS操做把對象當前的Mark Word和線程中複製的Displaced Mark Word替換回來,若是成功,則同步過程完成。 (2)CAS替換失敗,說明有其餘線程嘗試過獲取該鎖,那就要在釋放鎖的同時,喚醒被掛起的線程。
輕量級鎖能提高同步性能的依據是「對於絕大部分的鎖,在整個同步週期都是不存在競爭的」。若果沒有競爭,輕量級鎖使用CAS操做避免了使用互斥量的開銷,但若是存在鎖競爭,除了互斥量的開銷外,還額外發成了CAS操做,所以存在競爭的狀況下,輕量級鎖比傳統的重量級作會更慢。
<font color="#EE30A7">重量級鎖</font>
重量級鎖經過對象內部的監視器(monitor)實現,其中monitor的本質是依賴於底層操做系統的Mutex Lock實現,操做系統實現線程之間的切換須要從用戶態到內核態的切換,切換成本很是高。
<font color="#EE30A7">偏向鎖、輕量級鎖的狀態轉換</font>
<font color="#EE30A7">其它優化</font>
-
自旋鎖
線程的掛起和恢復須要CPU從用戶狀態切換到核心狀態,頻繁的掛起和恢復會給系統的併發性能帶來很大的壓力。同時咱們發如今許多的應用上,共享該數據的鎖定只會持續很短的一段時間,爲了這一段很短的時間,讓線程頻繁的掛起和恢復是很不值得的,所以引入了自旋鎖。 自旋鎖的原理很是的簡單,若果那些持有鎖的線程可以在很短的時間釋放資源,那麼那等待競爭鎖的線程就不須要作用戶狀態和內核狀態的切換進入阻塞掛起狀態,它們只須要「稍等一下」,等待持有鎖的線程釋放資源後當即獲取鎖。這裏須要注意的是,線程在自旋的過程當中,是不會放棄CPU的執行時間的,所以若是鎖被佔用的時間很長,那麼自旋的線程不作任何有用的工做從而浪費了CPU的資源。全部自旋等待時間必須有一個限制,若是自旋超過了限定的次數任然沒有獲取鎖,則須要中止自旋進入阻塞狀態。虛擬機設定的自旋次數默認是10次,能夠經過 -XX:PreBlockSpin來更改。
-
自適自旋鎖
上面說到自旋鎖的自旋次數是一個固定的值,可是這個自旋次數應該如何限定了,設置大了會讓線程一直佔用CPU時間浪費性能,設置低了會讓線程頻繁的進入掛起和恢復狀態也會浪費性能。所以JDK在1.6中引入了自適應自旋鎖,自適應說明自旋的時間不在是固定的了,而是由前一次在同一個鎖上的自旋時間以及鎖的擁有者的狀態來決定的。 自適應自旋鎖的原理也很是簡單,當一個線程在一把鎖上自旋成功,那麼下一次在這個鎖上自旋的時間將更長,由於虛擬機認爲上次自旋成功了,那麼此次自旋也有可能再次成功。反之,若是一個線程在一個鎖上不多自旋成功,那麼之後這個線程要獲取這個鎖時,自旋的此時將會減小甚至可能省略自旋的過程,直接進入阻塞狀態以避免浪費CPU的資源。
-
鎖消除
鎖消除是指虛擬機即時編譯器在運行時,對一些代碼上要求同步,可是被檢測到不可能存在共享數據競爭的鎖進行消除。鎖消除的主要斷定依據是逃逸分析的數據支持。變量是否逃逸對於虛擬機來講須要使用數據流來分析,可是對於咱們程序員應該是很清楚的,怎麼會在知道不存在數據競爭的狀況下使用同步呢?可是程序有時並非咱們想的那樣,雖然咱們沒有顯示的使用鎖,可是在使用一些Java 的API時,會存在隱式加鎖的狀況。例如以下代碼:
public String concat(String s1, String s2){ StringBuffer sb = new StringBuffer(); sb.append(s1); sb.append(s2); return sb.toString(); }
咱們知道每一個sb.append()方法中都有一個同步快,鎖就是sb的對象。所以虛擬機在運行這段代碼時,會監測到sb這個變量永遠不會「逃逸」到concat()方法以外,所以虛擬機就會消除這段代碼中的鎖而直接執行了。
-
鎖粗化
咱們知道在使用同步鎖的時候,須要儘可能將同步塊的做用範圍限制的儘可能小一些----只在共享數據的實際做用域中才進行同步,這樣作的目的是爲了是同步的時間儘量的縮短,若是存在鎖的競爭,那麼等待鎖的線程也能儘快的獲取到鎖。 大多數狀況下,上面的的原則都是正確的。可是若是一系列的連續操做都對同一個對象反覆的加鎖,甚至加鎖出如今循環體中,那麼即時沒有競爭,頻繁的進行互斥同步操做也會致使沒必要須的性能損耗。因此引入了鎖粗化的機率。 那麼什麼是鎖粗化呢?鎖粗化就是將鏈接加鎖、解鎖的過程鏈接在一塊兒,擴展(粗化)成爲一個同步範圍更大的鎖。以上面代碼爲例,就是擴展到第一個append()操做以前,直至最後一個append()操做以後,這樣只須要加鎖一次就能夠了。
<font color="#EE30A7">總結</font>
本文重點探究了Synchronized的實現原理,以及JDK引入偏向鎖和輕量級鎖對synchronized所作的優化處理,和一些其餘的鎖的優化處理。咱們最後來總結一下Synchronized的執行過程:
- 檢測Mark Word裏面是否是當前線程的ID,若是是,表示當前線程處於偏向鎖 。
- 若是不是,則使用CAS將當前線程的ID替換Mard Word,若是成功則表示當前線程得到偏向鎖,置偏向標誌位1 。
- 若是失敗,則說明發生競爭,撤銷偏向鎖,進而升級爲輕量級鎖。
- 當前線程使用CAS將對象頭的Mark Word替換爲鎖記錄指針,若是成功,當前線程得到鎖 。
- 若是失敗,表示其餘線程競爭鎖,當前線程便嘗試使用自旋來獲取鎖。
- 若是自旋成功則依然處於輕量級狀態。
- 若是自旋失敗,則升級爲重量級鎖。