深入理解synchronized底層原理,一篇文章就夠了!

前言

如果某一個資源被多個線程共享,爲了避免因爲資源搶佔導致資源數據錯亂,我們需要對線程進行同步,那麼synchronized就是實現線程同步的關鍵字,可以說在併發控制中是必不可少的部分,今天就來看一下synchronized的使用和底層原理。

一、synchronized的特性

1.1 原子性

所謂原子性就是指一個操作或者多個操作,要麼全部執行並且執行的過程不會被任何因素打斷,要麼就都不執行。

在Java中,對基本數據類型的變量的讀取和賦值操作是原子性操作,即這些操作是不可被中斷的,要麼執行,要麼不執行。但是像i++i+=1等操作字符就不是原子性的,它們是分成讀取、計算、賦值幾步操作,原值在這些步驟還沒完成時就可能已經被賦值了,那麼最後賦值寫入的數據就是髒數據,無法保證原子性。被synchronized修飾的類或對象的所有操作都是原子的,因爲在執行操作之前必須先獲得類或對象的鎖,直到執行完才能釋放,這中間的過程無法被中斷(除了已經廢棄的stop()方法),即保證了原子性。

注意!面試時經常會問比較synchronized和volatile,它們倆特性上最大的區別就在於原子性,volatile不具備原子性。

1.2 可見性

可見性是指多個線程訪問一個資源時,該資源的狀態、值信息等對於其他線程都是可見的。

synchronized和volatile都具有可見性,其中synchronized對一個類或對象加鎖時,一個線程如果要訪問該類或對象必須先獲得它的鎖,而這個鎖的狀態對於其他任何線程都是可見的,並且在釋放鎖之前會將對變量的修改刷新到主存當中,保證資源變量的可見性,如果某個線程佔用了該鎖,其他線程就必須在鎖池中等待鎖的釋放。

而volatile的實現類似,被volatile修飾的變量,每當值需要修改時都會立即更新主存,主存是共享的,所有線程可見,所以確保了其他線程讀取到的變量永遠是最新值,保證可見性。

1.3 有序性

有序性值程序執行的順序按照代碼先後執行。

synchronized和volatile都具有有序性,Java允許編譯器和處理器對指令進行重排,但是指令重排並不會影響單線程的順序,它影響的是多線程併發執行的順序性。synchronized保證了每個時刻都只有一個線程訪問同步代碼塊,也就確定了線程執行同步代碼塊是分先後順序的,保證了有序性。

1.4 可重入性

synchronized和ReentrantLock都是可重入鎖。當一個線程試圖操作一個由其他線程持有的對象鎖的臨界資源時,將會處於阻塞狀態,但當一個線程再次請求自己持有對象鎖的臨界資源時,這種情況屬於重入鎖。通俗一點講就是說一個線程擁有了鎖仍然還可以重複申請鎖。

二、synchronized的用法

synchronized可以修飾靜態方法、成員函數,同時還可以直接定義代碼塊,但是歸根結底它上鎖的資源只有兩類:一個是對象,一個是

先看看下面的代碼(初學者看到先不要暈,後面慢慢講解):

首先我們知道被static修飾的靜態方法、靜態屬性都是歸類所有,同時該類的所有實例對象都可以訪問。但是普通成員屬性、成員方法是歸實例化的對象所有,必須實例化之後才能訪問,這也是爲什麼靜態方法不能訪問非靜態屬性的原因。我們明確了這些屬性、方法歸哪些所有之後就可以理解上面幾個synchronized的鎖到底是加給誰的了。

首先看第一個synchronized所加的方法是add1(),該方法沒有被static修飾,也就是說該方法是歸實例化的對象所有,那麼這個鎖就是加給Test1類所實例化的對象。

然後是add2()方法,該方法是靜態方法,歸Test1類所有,所以這個鎖是加給Test1類的。

最後是method()方法中兩個同步代碼塊,第一個代碼塊所鎖定的是Test1.class,通過字面意思便知道該鎖是加給Test1類的,而下面那個鎖定的是instance,這個instance是Test1類的一個實例化對象,自然它所上的鎖是給instance實例化對象的。

弄清楚這些鎖是上給誰的就應該很容易懂synchronized的使用啦,只要記住要進入同步方法或同步塊必須先獲得相應的鎖才行。那麼我下面再列舉出一個非常容易進入誤區的代碼,看看你是否真的理解了上面的解釋。

上面的簡單意思就是用兩個線程分別對i加100萬次,理論結果應該是200萬,而且我還加了synchronized鎖住了add方法,保證了其線程安全性。可是!!!我無論運行多少次都是小於200萬的,爲什麼呢?

原因就在於synchronized加鎖的函數,這個方法是普通成員方法,那麼鎖就是加給對象的,但是在創建線程時卻new了兩個Test2實例,也就是說這個鎖是給這兩個實例加的鎖,並沒有達到同步的效果,所以纔會出現錯誤。至於爲什麼小於200萬,要理解i++的過程就明白了,我之前寫了一篇文章講解過這個過程,請閱讀:詳談Java中的CAS操作

三、synchronized鎖的實現

synchronized有兩種形式上鎖,一個是對方法上鎖,一個是構造同步代碼塊。他們的底層實現其實都一樣,在進入同步代碼之前先獲取鎖,獲取到鎖之後鎖的計數器+1,同步代碼執行完鎖的計數器-1,如果獲取失敗就阻塞式等待鎖的釋放。只是他們在同步塊識別方式上有所不一樣,從class字節碼文件可以表現出來,一個是通過方法flags標誌,一個是monitorenter和monitorexit指令操作。

3.1 同步方法

首先來看在方法上上鎖,我們就新定義一個同步方法然後進行反編譯,查看其字節碼:

可以看到在add方法的flags裏面多了一個ACC_SYNCHRONIZED標誌,這標誌用來告訴JVM這是一個同步方法,在進入該方法之前先獲取相應的鎖,鎖的計數器加1,方法結束後計數器-1,如果獲取失敗就阻塞住,知道該鎖被釋放。

如果看不懂字節碼指令的朋友可以先閱讀我之前寫的兩篇文章,瞭解一下class的結構:

3.2 同步代碼塊

我們新定義一個同步代碼塊,編譯出class字節碼,然後找到method方法所在的指令塊,可以清楚的看到其實現上鎖和釋放鎖的過程,截圖如下:

從反編譯的同步代碼塊可以看到同步塊是由monitorenter指令進入,然後monitorexit釋放鎖,在執行monitorenter之前需要嘗試獲取鎖,如果這個對象沒有被鎖定,或者當前線程已經擁有了這個對象的鎖,那麼就把鎖的計數器加1。當執行monitorexit指令時,鎖的計數器也會減1。當獲取鎖失敗時會被阻塞,一直等待鎖被釋放。

但是爲什麼會有兩個monitorexit呢?其實第二個monitorexit是來處理異常的,仔細看反編譯的字節碼,正常情況下第一個monitorexit之後會執行goto指令,而該指令轉向的就是23行的return,也就是說正常情況下只會執行第一個monitorexit釋放鎖,然後返回。而如果在執行中發生了異常,第二個monitorexit就起作用了,它是由編譯器自動生成的,在發生異常時處理異常然後釋放掉鎖。

四、synchronized鎖的底層實現

在理解鎖實現原理之前先了解一下Java的對象頭和Monitor,在JVM中,對象是分成三部分存在的:對象頭、實例數據、對其填充。

實例數據和對其填充與synchronized無關,這裏簡單說一下(我也是閱讀《深入理解Java虛擬機》學到的,讀者可仔細閱讀該書相關章節學習)。實例數據存放類的屬性數據信息,包括父類的屬性信息,如果是數組的實例部分還包括數組的長度,這部分內存按4字節對齊;對其填充不是必須部分,由於虛擬機要求對象起始地址必須是8字節的整數倍,對齊填充僅僅是爲了使字節對齊。

對象頭是我們需要關注的重點,它是synchronized實現鎖的基礎,因爲synchronized申請鎖、上鎖、釋放鎖都與對象頭有關。對象頭主要結構是由Mark WordClass Metadata Address 組成,其中Mark Word存儲對象的hashCode、鎖信息或分代年齡或GC標誌等信息Class Metadata Address是類型指針指向對象的類元數據,JVM通過該指針確定該對象是哪個類的實例

鎖也分不同狀態,JDK6之前只有兩個狀態:無鎖、有鎖(重量級鎖),而在JDK6之後對synchronized進行了優化,新增了兩種狀態,總共就是四個狀態:無鎖狀態、偏向鎖、輕量級鎖、重量級鎖,其中無鎖就是一種狀態了。鎖的類型和狀態在對象頭Mark Word中都有記錄,在申請鎖、鎖升級等過程中JVM都需要讀取對象的Mark Word數據。

每一個鎖都對應一個monitor對象,在HotSpot虛擬機中它是由ObjectMonitor實現的(C++實現)。每個對象都存在着一個monitor與之關聯,對象與其monitor之間的關係有存在多種實現方式,如monitor可以與對象一起創建銷燬或當線程試圖獲取對象鎖時自動生成,但當一個monitor被某個線程持有後,它便處於鎖定狀態。

ObjectMonitor() {
    _header       = NULL;
    _count        = 0;  //鎖計數器
    _waiters      = 0,
    _recursions   = 0;
    _object       = NULL;
    _owner        = NULL;
    _WaitSet      = NULL; //處於wait狀態的線程,會被加入到_WaitSet
    _WaitSetLock  = 0 ;
    _Responsible  = NULL ;
    _succ         = NULL ;
    _cxq          = NULL ;
    FreeNext      = NULL ;
    _EntryList    = NULL ; //處於等待鎖block狀態的線程,會被加入到該列表
    _SpinFreq     = 0 ;
    _SpinClock    = 0 ;
    OwnerIsThread = 0 ;
  }

該段摘自:https://blog.csdn.net/javazejian/article/details/72828483

  ObjectMonitor中有兩個隊列_WaitSet和_EntryList,用來保存ObjectWaiter對象列表(每個等待鎖的線程都會被封裝ObjectWaiter對象),_owner指向持有ObjectMonitor對象的線程,當多個線程同時訪問一段同步代碼時,首先會進入_EntryList 集合,當線程獲取到對象的monitor 後進入 _Owner 區域並把monitor中的owner變量設置爲當前線程同時monitor中的計數器count加1,若線程調用 wait() 方法,將釋放當前持有的monitor,owner變量恢復爲null,count自減1,同時該線程進入 WaitSe t集合中等待被喚醒。若當前線程執行完畢也將釋放monitor(鎖)並復位變量的值,以便其他線程進入獲取monitor(鎖)。

  monitor對象存在於每個Java對象的對象頭中(存儲的指針的指向),synchronized鎖便是通過這種方式獲取鎖的,也是爲什麼Java中任意對象可以作爲鎖的原因,同時也是notify/notifyAll/wait等方法存在於頂級對象Object中的原因(關於這點稍後還會進行分析)

五、JVM對synchronized的優化

從最近幾個jdk版本中可以看出,Java的開發團隊一直在對synchronized優化,其中最大的一次優化就是在jdk6的時候,新增了兩個鎖狀態,通過鎖消除、鎖粗化、自旋鎖等方法使用各種場景,給synchronized性能帶來了很大的提升。

5.1 鎖膨脹

上面講到鎖有四種狀態,並且會因實際情況進行膨脹升級,其膨脹方向是:無鎖——>偏向鎖——>輕量級鎖——>重量級鎖,並且膨脹方向不可逆。

5.1.1 偏向鎖

一句話總結它的作用:減少統一鮮橙獲取鎖的代價。在大多數情況下,鎖不存在多線程競爭,總是由同一鮮橙多次獲得,那麼此時就是偏向鎖。

核心思想:

如果一個線程獲得了鎖,那麼鎖就進入偏向模式,此時Mark Word的結構也就變爲偏向鎖結構,當該線程再次請求鎖時,無需再做任何同步操作,即獲取鎖的過程只需要檢查Mark Word的鎖標記位爲偏向鎖以及當前線程ID等於Mark Word的ThreadID即可,這樣就省去了大量有關鎖申請的操作。

5.1.2 輕量級鎖

輕量級鎖是由偏向鎖升級而來,當存在第二個線程申請同一個鎖對象時,偏向鎖就會立即升級爲輕量級鎖。注意這裏的第二個線程只是申請鎖,不存在兩個線程同時競爭鎖,可以是一前一後地交替執行同步塊。

5.1.3 重量級鎖

重量級鎖是由輕量級鎖升級而來,當同一時間有多個線程競爭鎖時,鎖就會被升級成重量級鎖,此時其申請鎖帶來的開銷也就變大。

重量級鎖一般使用場景會在追求吞吐量,同步塊或者同步方法執行時間較長的場景。

5.2 鎖消除

**消除鎖是虛擬機另外一種鎖的優化,這種優化更徹底,在JIT編譯時,對運行上下文進行掃描,去除不可能存在競爭的鎖。**比如下面代碼的method1和method2的執行效率是一樣的,因爲object鎖是私有變量,不存在所得競爭關係。

5.3 鎖粗化

**鎖粗化是虛擬機對另一種極端情況的優化處理,通過擴大鎖的範圍,避免反覆加鎖和釋放鎖。**比如下面method3經過鎖粗化優化之後就和method4執行效率一樣了。

5.4 自旋鎖與自適應自旋鎖

輕量級鎖失敗後,虛擬機爲了避免線程真實地在操作系統層面掛起,還會進行一項稱爲自旋鎖的優化手段。

**自旋鎖:**許多情況下,共享數據的鎖定狀態持續時間較短,切換線程不值得,通過讓線程執行循環等待鎖的釋放,不讓出CPU。如果得到鎖,就順利進入臨界區。如果還不能獲得鎖,那就會將線程在操作系統層面掛起,這就是自旋鎖的優化方式。但是它也存在缺點:如果鎖被其他線程長時間佔用,一直不釋放CPU,會帶來許多的性能開銷。

自適應自旋鎖:這種相當於是對上面自旋鎖優化方式的進一步優化,它的自旋的次數不再固定,其自旋的次數由前一次在同一個鎖上的自旋時間及鎖的擁有者的狀態來決定,這就解決了自旋鎖帶來的缺點。

結語

synchronized關鍵字是併發編程不可或缺的部分,個人認爲能真實理解其內部運作原理能對平時的開發帶來很大意義上的幫助,希望這篇文章能幫助你!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章