若是某一個資源被多個線程共享,爲了不由於資源搶佔致使資源數據錯亂,咱們須要對線程進行同步,那麼synchronized就是實現線程同步的關鍵字,能夠說在併發控制中是必不可少的部分,今天就來看一下synchronized的使用和底層原理。javascript
所謂原子性就是指一個操做或者多個操做,要麼所有執行而且執行的過程不會被任何因素打斷,要麼就都不執行。java
在Java中,對基本數據類型的變量的讀取和賦值操做是原子性操做,即這些操做是不可被中斷的,要麼執行,要麼不執行。可是像i++、i+=1等操做字符就不是原子性的,它們是分紅讀取、計算、賦值幾步操做,原值在這些步驟還沒完成時就可能已經被賦值了,那麼最後賦值寫入的數據就是髒數據,沒法保證原子性。面試
被synchronized修飾的類或對象的全部操做都是原子的,由於在執行操做以前必須先得到類或對象的鎖,直到執行完才能釋放,這中間的過程沒法被中斷(除了已經廢棄的stop()方法),即保證了原子性。數組
注意!面試時常常會問比較synchronized和volatile,它們倆特性上最大的區別就在於原子性,volatile不具有原子性。安全
可見性是指多個線程訪問一個資源時,該資源的狀態、值信息等對於其餘線程都是可見的。多線程
synchronized和volatile都具備可見性,其中synchronized對一個類或對象加鎖時,一個線程若是要訪問該類或對象必須先得到它的鎖,而這個鎖的狀態對於其餘任何線程都是可見的,而且在釋放鎖以前會將對變量的修改刷新到主存當中,保證資源變量的可見性,若是某個線程佔用了該鎖,其餘線程就必須在鎖池中等待鎖的釋放。併發
而volatile的實現相似,被volatile修飾的變量,每當值須要修改時都會當即更新主存,主存是共享的,全部線程可見,因此確保了其餘線程讀取到的變量永遠是最新值,保證可見性。函數
有序性值程序執行的順序按照代碼前後執行。性能
synchronized和volatile都具備有序性,Java容許編譯器和處理器對指令進行重排,可是指令重排並不會影響單線程的順序,它影響的是多線程併發執行的順序性。synchronized保證了每一個時刻都只有一個線程訪問同步代碼塊,也就肯定了線程執行同步代碼塊是分前後順序的,保證了有序性。學習
synchronized和ReentrantLock都是可重入鎖。當一個線程試圖操做一個由其餘線程持有的對象鎖的臨界資源時,將會處於阻塞狀態,但當一個線程再次請求本身持有對象鎖的臨界資源時,這種狀況屬於重入鎖。通俗一點講就是說一個線程擁有了鎖仍然還能夠重複申請鎖。
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有兩種形式上鎖,一個是對方法上鎖,一個是構造同步代碼塊。他們的底層實現其實都同樣,在進入同步代碼以前先獲取鎖,獲取到鎖以後鎖的計數器+1,同步代碼執行完鎖的計數器-1,若是獲取失敗就阻塞式等待鎖的釋放。只是他們在同步塊識別方式上有所不同,從class字節碼文件能夠表現出來,一個是經過方法flags標誌,一個是monitorenter和monitorexit指令操做。
首先來看在方法上上鎖,咱們就新定義一個同步方法而後進行反編譯,查看其字節碼:
能夠看到在add方法的flags裏面多了一個ACC_SYNCHRONIZED
標誌,這標誌用來告訴JVM這是一個同步方法,在進入該方法以前先獲取相應的鎖,鎖的計數器加1,方法結束後計數器-1,若是獲取失敗就阻塞住,知道該鎖被釋放。
若是看不懂字節碼指令的朋友能夠先閱讀我以前寫的兩篇文章,瞭解一下class的結構:
咱們新定義一個同步代碼塊,編譯出class字節碼,而後找到method方法所在的指令塊,能夠清楚的看到其實現上鎖和釋放鎖的過程,截圖以下:
從反編譯的同步代碼塊能夠看到同步塊是由monitorenter指令進入,而後monitorexit釋放鎖,在執行monitorenter以前須要嘗試獲取鎖,若是這個對象沒有被鎖定,或者當前線程已經擁有了這個對象的鎖,那麼就把鎖的計數器加1。當執行monitorexit指令時,鎖的計數器也會減1。當獲取鎖失敗時會被阻塞,一直等待鎖被釋放。
可是爲何會有兩個monitorexit呢?其實第二個monitorexit是來處理異常的,仔細看反編譯的字節碼,正常狀況下第一個monitorexit以後會執行goto
指令,而該指令轉向的就是23行的return
,也就是說正常狀況下只會執行第一個monitorexit釋放鎖,而後返回。而若是在執行中發生了異常,第二個monitorexit就起做用了,它是由編譯器自動生成的,在發生異常時處理異常而後釋放掉鎖。
在理解鎖實現原理以前先了解一下Java的對象頭和Monitor,在JVM中,對象是分紅三部分存在的:對象頭、實例數據、對其填充。
實例數據和對其填充與synchronized無關,這裏簡單說一下(我也是閱讀《深刻理解Java虛擬機》學到的,讀者可仔細閱讀該書相關章節學習)。實例數據存放類的屬性數據信息,包括父類的屬性信息,若是是數組的實例部分還包括數組的長度,這部份內存按4字節對齊;對其填充不是必須部分,因爲虛擬機要求對象起始地址必須是8字節的整數倍,對齊填充僅僅是爲了使字節對齊。
對象頭是咱們須要關注的重點,它是synchronized實現鎖的基礎,由於synchronized申請鎖、上鎖、釋放鎖都與對象頭有關。對象頭主要結構是由Mark Word
和 Class 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中的緣由(關於這點稍後還會進行分析)
從最近幾個jdk版本中能夠看出,Java的開發團隊一直在對synchronized優化,其中最大的一次優化就是在jdk6的時候,新增了兩個鎖狀態,經過鎖消除、鎖粗化、自旋鎖等方法使用各類場景,給synchronized性能帶來了很大的提高。
上面講到鎖有四種狀態,而且會因實際狀況進行膨脹升級,其膨脹方向是:無鎖——>偏向鎖——>輕量級鎖——>重量級鎖,而且膨脹方向不可逆。
一句話總結它的做用:減小統一線程獲取鎖的代價。在大多數狀況下,鎖不存在多線程競爭,老是由同一線程屢次得到,那麼此時就是偏向鎖。
核心思想:
若是一個線程得到了鎖,那麼鎖就進入偏向模式,此時Mark Word
的結構也就變爲偏向鎖結構,當該線程再次請求鎖時,無需再作任何同步操做,即獲取鎖的過程只須要檢查Mark Word
的鎖標記位爲偏向鎖以及當前線程ID等於Mark Word
的ThreadID便可,這樣就省去了大量有關鎖申請的操做。
輕量級鎖是由偏向鎖升級而來,當存在第二個線程申請同一個鎖對象時,偏向鎖就會當即升級爲輕量級鎖。注意這裏的第二個線程只是申請鎖,不存在兩個線程同時競爭鎖,能夠是一前一後地交替執行同步塊。
重量級鎖是由輕量級鎖升級而來,當同一時間有多個線程競爭鎖時,鎖就會被升級成重量級鎖,此時其申請鎖帶來的開銷也就變大。
重量級鎖通常使用場景會在追求吞吐量,同步塊或者同步方法執行時間較長的場景。
消除鎖是虛擬機另一種鎖的優化,這種優化更完全,在JIT編譯時,對運行上下文進行掃描,去除不可能存在競爭的鎖。好比下面代碼的method1和method2的執行效率是同樣的,由於object鎖是私有變量,不存在所得競爭關係。
鎖粗化是虛擬機對另外一種極端狀況的優化處理,經過擴大鎖的範圍,避免反覆加鎖和釋放鎖。好比下面method3通過鎖粗化優化以後就和method4執行效率同樣了。
輕量級鎖失敗後,虛擬機爲了不線程真實地在操做系統層面掛起,還會進行一項稱爲自旋鎖的優化手段。
自旋鎖:許多狀況下,共享數據的鎖定狀態持續時間較短,切換線程不值得,經過讓線程執行循環等待鎖的釋放,不讓出CPU。若是獲得鎖,就順利進入臨界區。若是還不能得到鎖,那就會將線程在操做系統層面掛起,這就是自旋鎖的優化方式。可是它也存在缺點:若是鎖被其餘線程長時間佔用,一直不釋放CPU,會帶來許多的性能開銷。
自適應自旋鎖:這種至關因而對上面自旋鎖優化方式的進一步優化,它的自旋的次數再也不固定,其自旋的次數由前一次在同一個鎖上的自旋時間及鎖的擁有者的狀態來決定,這就解決了自旋鎖帶來的缺點。