深入剖析Java中的synchronized關(guān)鍵字
synchronized介紹
synchronized
關(guān)鍵字可以解決的是多個線程之間訪問資源的同步性。synchronized
關(guān)鍵字可以保證被它修飾的方法或者代碼塊在任意時刻只能有一個線程執(zhí)行。
在 Java 程序中,我們可以利用 synchronized 關(guān)鍵字來對程序進(jìn)行加鎖。它既可以用來聲明一個 synchronized 代碼塊,也可以直接標(biāo)記靜態(tài)方法或者實例方法。
關(guān)鍵字在代碼塊上
代碼如下:
public void methodA() { Object obj = new Object(); synchronized (obj) { // } }
編譯結(jié)果(javap -v)
public void methodA(); descriptor: ()V flags: ACC_PUBLIC Code: stack=2, locals=4, args_size=1 0: new #3 // class java/lang/Object 3: dup 4: invokespecial #1 // Method java/lang/Object."<init>":()V 7: astore_1 8: aload_1 9: dup 10: astore_2 11: monitorenter 12: aload_2 13: monitorexit 14: goto 22 17: astore_3 18: aload_2 19: monitorexit 20: aload_3 21: athrow 22: return Exception table: from to target type 12 14 17 any 17 20 17 any
上面的字節(jié)碼中包含一個 monitorenter 指令以及多個 monitorexit 指令。這是因為 Java 虛擬機(jī)需要確保所獲得的鎖在正常執(zhí)行路徑,以及異常執(zhí)行路徑上都能夠被解鎖。 synchronized 應(yīng)用在同步塊上時,在字節(jié)碼中是通過 monitorenter 和 monitorexit 實現(xiàn)的。
關(guān)鍵字在方法上
代碼如下:
public synchronized void methodB() { // i++; }
當(dāng)synchronized修飾同步方法時,編譯器會在生成的字節(jié)碼中添加一個額外的指令來獲取和釋放方法的監(jiān)視器鎖(monitor lock)。同時,編譯器還會設(shè)置方法的ACC_SYNCHRONIZED標(biāo)志。
public synchronized void methodB(); descriptor: ()V flags: ACC\_PUBLIC, ACC\_SYNCHRONIZED Code: stack=3, locals=1, args\_size=1 0: aload\_0 1: dup 2: getfield #2 // Field i:I 5: iconst\_1 6: iadd 7: putfield #2 // Field i:I 10: return LineNumberTable: line 15: 0 line 16: 10
當(dāng)JVM加載字節(jié)碼文件并解析類的時候,會檢查方法的訪問標(biāo)志。如果ACC_SYNCHRONIZED標(biāo)志被設(shè)置,表示在進(jìn)入該方法時,Java 虛擬機(jī)需要進(jìn)行 monitorenter 操作。而在退出該方法時,不管是正常返回,還是向調(diào)用者拋異常,Java 虛擬機(jī)均需要進(jìn)行 monitorexit 操作。
這里 monitorenter 和 monitorexit 操作所對應(yīng)的鎖對象是隱式的。對于實例方法來說,這兩個操作對應(yīng)的鎖對象是 this;對于靜態(tài)方法來說,這兩個操作對應(yīng)的鎖對象則是所在類的 Class 實例。
synchronized原理
synchronized 對對象進(jìn)行加鎖,在 JVM 中,對象在內(nèi)存中分為三塊區(qū)域:對象頭、實例數(shù)據(jù)和對齊填充。在對象頭中保存了鎖標(biāo)志位和指向 monitor 對象的起始地址,如下圖所示,右側(cè)就是對象對應(yīng)的 Monitor 對象。當(dāng) Monitor 被某個線程持有后,就會處于鎖定狀態(tài),如圖中的 Owner 部分,會指向持有 Monitor 對象的線程。另外 Monitor 中還有兩個隊列,用來存放進(jìn)入及等待獲取鎖的線程。
為了提升性能,JDK1.6 引入了偏向鎖、輕量級鎖、重量級鎖概念,來減少鎖競爭帶來的上下文切換,而正是新增的 Java 對象頭實現(xiàn)了鎖升級功能。當(dāng) Java 對象被 Synchronized 關(guān)鍵字修飾成為同步鎖后,圍繞這個鎖的一系列升級操作都將和 Java 對象頭有關(guān)。
對象頭
Java中對象頭由三個部分組成:Mark Word、Klass Pointer、Length。
Mark Word
Mark WordMark Word記錄了與對象和鎖相關(guān)的信息,當(dāng)這個對象作為鎖對象來實現(xiàn)synchronized的同步操作時,鎖標(biāo)記和相關(guān)信息都是存儲在Mark Word中的。64位系統(tǒng)Mark Word存儲結(jié)構(gòu)如下:
從圖中可以看到一個鎖狀態(tài)的字段,它包含五種狀態(tài)分別是無鎖、偏向鎖、輕量級鎖、重量級鎖、GC標(biāo)記。通過1bit來表達(dá)無鎖和偏向鎖,其中0表示無鎖、1表示偏向鎖。
Klass PointerKlass Pointer
Klass PointerKlass Pointer表示指向類的指針,JVM通過這個指針來確定對象具體屬于哪個類的實例。它的存儲長度根據(jù)JVM的位數(shù)來決定,在32位的虛擬機(jī)中占4字節(jié),在64位的虛擬機(jī)中占8字節(jié),但是在JDK 1.8中,由于默認(rèn)開啟了指針壓縮,所以壓縮后在64位系統(tǒng)中只占4字節(jié)。Length
Length表示數(shù)組長度,只有構(gòu)建對象數(shù)組時才會有數(shù)組長度屬性。
鎖升級過程
當(dāng)一個線程訪問增加了synchronized關(guān)鍵字的代碼塊時,如果偏向鎖是開啟狀態(tài),則先嘗試通過偏向鎖來獲得鎖資源,這個過程僅僅通過CAS來完成。如果當(dāng)前已經(jīng)有其他線程獲得了偏向鎖,那么搶占鎖資源的線程由于無法獲得鎖,所以會嘗試升級到輕量級鎖來進(jìn)行鎖資源搶占,輕量級鎖就是通過多次CAS(也就是自旋鎖)來完成的。如果這個線程通過多次自旋仍然無法獲得鎖資源,那么最終只能升級到重量級鎖來實現(xiàn)線程的等待。
下圖顯示了對象頭的布局和不同對象狀態(tài)的表示:
偏向鎖的原理
偏向鎖其實可以認(rèn)為是在沒有多線程競爭的情況下訪問synchronized修飾的代碼塊的加鎖場景,也就是在單線程執(zhí)行的情況下。
實際上對程序開發(fā)來說,加鎖是為了防范線程安全性的風(fēng)險,但是是否有線程競爭并不由我們來控制,而是由應(yīng)用場景來決定。假設(shè)這種情況存在,就沒有必要使用重量級鎖基于操作系統(tǒng)級別的Mutex Lock來實現(xiàn)鎖的搶占,這樣顯然很耗費性能。
所以偏向鎖的作用就是,線程在沒有線程競爭的情況下去訪問synchronized同步代碼塊時,會嘗試先通過偏向鎖來搶占訪問資格,這個搶占過程是基于CAS來完成的,如果搶占鎖成功,則直接修改對象頭中的鎖標(biāo)記。其中,偏向鎖標(biāo)記為1,鎖標(biāo)記為01,以及存儲當(dāng)前獲得鎖的線程ID。而偏向的意思就是,如果線程X獲得了偏向鎖,那么當(dāng)線程X后續(xù)再訪問這個同步方法時,只需要判斷對象頭中的線程ID和線程X是否相等即
獲取偏向鎖的流程
下圖代表獲取偏向鎖的粗粒度流程圖,偏向鎖是在沒有線程競爭的情況下實現(xiàn)的一種鎖,不能排除存在鎖競爭的情況,所以偏向鎖的獲取有兩種情況。
沒有鎖競爭
在沒有鎖競爭并且開啟了偏向鎖的情況下,當(dāng)線程1訪問
synchronized(lock)
修飾的代碼塊時:- 從當(dāng)前線程的棧中找到一個空閑的BasicObjectLock(圖中Lock Record),它是一個基礎(chǔ)的鎖對象,在后續(xù)的輕量級鎖和重量級鎖中都會用到,BasicObjectLock包含以下兩個屬性。
- BasicLock,該屬性中有一個字段markOop,用于保存指向lock鎖對象的對象頭數(shù)據(jù)。
- oop,指向lock鎖對象的指針。
- 將BasicObjectLock中的oop指針指向當(dāng)前的鎖對象lock。
- 獲得當(dāng)前鎖對象lock的對象頭,通過對象頭來判斷是否可偏向,也就是說鎖標(biāo)記為101,并且Thread Id為空。
- 如果為可偏向狀態(tài),那么判斷當(dāng)前偏向的線程是不是線程1,如果偏向的是自己,則不需要再搶占鎖,直接有資格運行同步代碼塊。
- 如果為不可偏向狀態(tài),則需要通過輕量級鎖來完成鎖的搶占過程。
- 如果對象鎖lock偏向其他線程或者當(dāng)前是匿名偏向狀態(tài)(也就是沒有偏向任何一個線程),則先構(gòu)建一個匿名偏向的Mark Word,然后通過CAS方法,把一個匿名偏向的Mark Word修改為偏向線程1。如果當(dāng)前鎖對象lock已經(jīng)偏向了其他線程,那么CAS一定會失敗。
- 從當(dāng)前線程的棧中找到一個空閑的BasicObjectLock(圖中Lock Record),它是一個基礎(chǔ)的鎖對象,在后續(xù)的輕量級鎖和重量級鎖中都會用到,BasicObjectLock包含以下兩個屬性。
存在鎖競爭
假設(shè)線程1獲得了偏向鎖,此時線程2去執(zhí)行synchronized(lock)同步代碼塊,如果訪問到同一個對象鎖則會觸發(fā)鎖競爭并觸發(fā)偏向鎖撤銷,撤銷流程如下。
- 線程2調(diào)用撤銷偏向鎖方法,嘗試撤銷lock鎖對象的偏向鎖。
- 撤銷偏向鎖需要到達(dá)全局安全點(SafePoint)才會執(zhí)行,全局安全點就是當(dāng)前線程運行到的這個位置,線程的狀態(tài)可以被確定,堆對象的狀態(tài)也是確定的,在這個位置JVM可以安全地進(jìn)行GC、偏向鎖撤銷等動作。當(dāng)?shù)竭_(dá)全局安全點后,會暫停獲得偏向鎖的線程1。
- 檢查獲得偏向鎖的線程1的狀態(tài),這里存在兩種狀態(tài)。
- 線程1已經(jīng)執(zhí)行完同步代碼塊或者處于非存活狀態(tài)。在這種情況下,直接把偏向鎖撤銷恢復(fù)成無鎖狀態(tài),然后線程2升級到輕量級鎖,通過輕量級鎖搶占鎖資源。
- 線程1還在執(zhí)行同步代碼塊中的指令,也就是說沒有退出同步代碼塊。在這種情況下,直接把鎖對象lock升級成輕量級鎖(由于這里是全局安全點,所以不需要通過CAS來實現(xiàn)),并且指向線程1,表示線程1持有輕量級鎖,接著線程1繼續(xù)執(zhí)行同步代碼塊中的代碼。
偏向鎖的釋放
在偏向鎖執(zhí)行完synchronized同步代碼塊后,會觸發(fā)偏向鎖釋放的流程,需要注意的是,偏向鎖本質(zhì)上并沒有釋放,因為當(dāng)前鎖對象lock仍然是偏向該線程的。釋放的過程只是把Lock Record釋放了,也就是說把Lock Record保存的鎖對象的Mark Word設(shè)置為空。
偏向鎖批量重偏向當(dāng)一個鎖對象lock只被同一個線程訪問時,該鎖對象的鎖狀態(tài)就是偏向鎖,并且一直偏向該線程。當(dāng)有任何一個線程來訪問該鎖對象lock時,不管之前獲得偏向鎖線程的狀態(tài)是存活還是死亡,lock鎖對象都會升級為輕量級鎖,并且鎖在升級之后是不可逆的。
假設(shè)一個線程t1針對大量的鎖對象增加了偏向鎖,之后線程t2來訪問這些鎖對象,在不考慮鎖競爭的情況下,需要對之前所有偏向線程t1的鎖對象進(jìn)行偏向鎖撤銷和升級,這個過程比較耗時,而且虛擬機(jī)會認(rèn)為這個鎖不適合再偏向于原來的t1線程,于是當(dāng)偏向鎖撤銷次數(shù)達(dá)到20次時,會觸發(fā)批量重偏向,把所有的鎖對象全部偏向線程t2。偏向鎖撤銷并批量重偏向的觸發(fā)閾值可以通過XX:BiasedLockingBulkRebiasThreshold = 20
來配置,默認(rèn)是20。
在高并發(fā)場景下,當(dāng)大量線程同時競爭同一個鎖資源時,偏向鎖就會被撤銷,發(fā)生 stop the word 后, 開啟偏向鎖無疑會帶來更大的性能開銷,這時我們可以通過添加 JVM 參數(shù)關(guān)閉偏向鎖來調(diào)優(yōu)系統(tǒng)性能:
-XX:-UseBiasedLocking //關(guān)閉偏向鎖(默認(rèn)打開) 或者 -XX:+UseHeavyMonitors //設(shè)置重量級鎖
輕量級鎖的原理
在線程沒有競爭時,使用偏向鎖能夠在不影響性能的前提下獲得鎖資源,但是同一時刻只允許一個線程獲得鎖資源,如果有多個線程來訪問同步方法,于是就有了輕量級鎖的設(shè)計。
所謂的輕量級鎖,就是沒有搶占到鎖的線程,進(jìn)行一定次數(shù)的重試(CAS)。比如線程第一次沒搶到鎖則重試幾次,如果在重試的過程中搶占到了鎖,那么這個線程就不需要阻塞,這種實現(xiàn)方式我們稱為自旋鎖,具體的實現(xiàn)流程如圖所示。
線程通過重試來搶占鎖的方式是有代價的,因為線程如果不斷自旋重試,那么CPU會一直處于運行狀態(tài)。如果持有鎖的線程占有鎖的時間比較短,那么自旋等待的實現(xiàn)帶來性能的提升會比較明顯。反之,如果持有鎖的線程占用鎖資源的時間比較長,那么自旋的線程就會浪費CPU資源,所以線程重試搶占鎖的次數(shù)必須要有一個限制。從 JDK1.7 開始,自旋鎖默認(rèn)啟用,自旋次數(shù)由 JVM 設(shè)置決定,根據(jù)前一次在同一個鎖上的自旋次數(shù)及鎖持有者的狀態(tài)來決定的。如果在同一個鎖對象上,通過自旋等待成功獲得過鎖,并且持有鎖的線程正在運行中,那么JVM會認(rèn)為此次自旋也有很大的機(jī)會獲得鎖,因此會將這個線程的自旋時間相對延長。反之,如果在一個鎖對象中,通過自旋鎖獲得鎖很少成功,那么JVM會縮短自旋次數(shù)。
在高負(fù)載、高并發(fā)的場景下,我們可以通過設(shè)置 JVM 參數(shù)來關(guān)閉自旋鎖,優(yōu)化系統(tǒng)性能:
-XX:-UseSpinning //參數(shù)關(guān)閉自旋鎖優(yōu)化(默認(rèn)打開) -XX:PreBlockSpin //參數(shù)修改默認(rèn)的自旋次數(shù)。JDK1.7后,去掉此參數(shù),由jvm控制
如果偏向鎖存在競爭或者偏向鎖未開啟,那么當(dāng)線程訪問synchronized(lock)同步代碼塊時就會采用輕量級鎖來搶占鎖資源,獲得訪問資格,輕量級鎖的加鎖如圖所示:
獲取輕量級鎖的流程
在線程2進(jìn)入同步代碼塊后,JVM會給當(dāng)前線程分配一個Lock Record,也就是一個BasicObjectLock對象,在它的成員對象BasicLock中有一個成員屬性markOop _displaced_header,這個屬性專門用來保存鎖對象lock的原始Mark Word。
構(gòu)建一個無鎖狀態(tài)的Mark Word(其實就是lock鎖對象的Mark Word,但是鎖狀態(tài)是無鎖),把這個無鎖狀態(tài)的Mark Word設(shè)置到Lock Record中的_displaced_header字段中,如圖所示:
- 通過CAS將lock鎖對象的Mark Word替換為指向Lock Record的指針,如果替換成功,就會得到如圖所示的結(jié)構(gòu),表示輕量級鎖搶占成功,此時線程2可以執(zhí)行同步代碼塊。
如果CAS失敗,則說明當(dāng)前l(fā)ock鎖對象不是無鎖狀態(tài),會觸發(fā)鎖膨脹,升級到重量級鎖。
相對偏向鎖來說,輕量級鎖的原理比較簡單,它只是通過CAS來修改鎖對象中指向Lock Record的指針。從功能層面來說,偏向鎖和輕量級鎖最大的不同是:
- 偏向鎖只能保證偏向同一個線程,只要有線程獲得過偏向鎖,那么當(dāng)其他線程去搶占鎖時,只能通過輕量級鎖來實現(xiàn),除非觸發(fā)了重新偏向(如果獲得輕量級鎖的線程在后續(xù)的20次訪問中,發(fā)現(xiàn)每次訪問鎖的線程都是同一個,則會觸發(fā)重新偏向,20次的定義屬性為:
XX:BiasedLockingBulkRebiasThreshold =20)
。 - 輕量級鎖可以靈活釋放,也就是說,如果線程1搶占了輕量級鎖,那么在鎖用完并釋放后,線程2可以繼續(xù)通過輕量級鎖來搶占鎖資源。
輕量級鎖的釋放
偏向鎖也有鎖釋放的邏輯,但是它只是釋放Lock Record,原本的偏向關(guān)系仍然存在,所以并不是真正意義上的鎖釋放。而輕量級鎖釋放之后,其他線程可以繼續(xù)使用輕量級鎖來搶占鎖資源,具體的實現(xiàn)流程如下。
- 把Lock Record中_displaced_header存儲的lock鎖對象的Mark Word替換到lock鎖對象的Mark Word中,這個過程會采用CAS來完成。
- 如果CAS成功,則輕量級鎖釋放完成。
- 如果CAS失敗,說明釋放鎖的時候發(fā)生了競爭,就會觸發(fā)鎖膨脹,完成鎖膨脹之后,再調(diào)用重量級鎖的釋放鎖方法,完成鎖的釋放過程。
重量級鎖的原理分析
輕量級鎖能夠通過一定次數(shù)的重試讓沒有獲得鎖的線程有可能搶占到鎖資源,但是輕量級鎖只有在獲得鎖的線程持有鎖的時間較短的情況下才能起到提升同步鎖性能的效果。如果持有鎖的線程占用鎖資源的時間較長,那么不能讓那些沒有搶占到鎖資源的線程不斷自旋,否則會占用過多的CPU資源,這反而是一件得不償失的事情。如果沒搶占到鎖資源的線程通過一定次數(shù)的自旋后,發(fā)現(xiàn)仍然沒有獲得鎖,就只能阻塞等待了,所以最終會升級到重量級鎖,通過系統(tǒng)層面的互斥量(Mutex
)來搶占鎖資源。重量級鎖的實現(xiàn)原理如圖所示:
如果線程在運行synchronized(lock)同步代碼塊時,發(fā)現(xiàn)鎖狀態(tài)是輕量級鎖并且有其他線程搶占了鎖資源,那么該線程就會觸發(fā)鎖膨脹升級到重量級鎖。因此,重量級鎖是在存在線程競爭的場景中使用的鎖類型。
獲取重量級鎖的流程
重量級鎖的實現(xiàn)流程如圖所示:
重量級鎖的實現(xiàn)是在ObjectMonitor中完成的,所以鎖膨脹的意義就是構(gòu)建一個ObjectMonitor,繼續(xù)關(guān)注圖中ObjectMonitor的實現(xiàn)部分,在ObjectMonitor中鎖的實現(xiàn)過程如下:
- 首先,判斷當(dāng)前線程是否是重入,如果是則增加重入次數(shù)。
- 然后,通過自旋鎖來實現(xiàn)鎖的搶占(這個自旋鎖就是前面我們提到的自適應(yīng)自旋),這里使用CAS機(jī)制來判斷ObjectMonitor中的_owner字段是否為空,如果為空就表示重量級鎖已釋放,當(dāng)前線程可以獲得鎖,否則就進(jìn)行自適應(yīng)自旋重試。
- 最后,如果通過自旋鎖競爭鎖失敗,則會把當(dāng)前線程構(gòu)建成一個ObjectWaiter節(jié)點,插入_cxq隊列的隊首,再使用park方法阻塞當(dāng)前線程。
重量級鎖的釋放
鎖的釋放是在synchronized同步代碼塊結(jié)束后觸發(fā)的,釋放的邏輯比較簡單。
- 把ObjectMonitor中持有鎖的對象_owner置為null。
- 從_cxq隊列中喚醒一個處于鎖阻塞的線程。
- 被喚醒的線程會重新競爭重量級鎖,需要注意的是,synchronized是非公平鎖,因此被喚醒后不一定能夠搶占到鎖,如果沒搶到,則繼續(xù)等待。
總結(jié)
JVM 在 JDK1.6 中引入了分級鎖機(jī)制來優(yōu)化 Synchronized,當(dāng)一個線程獲取鎖時,首先對象鎖將成為一個偏向鎖,這樣做是為了優(yōu)化同一線程重復(fù)獲取導(dǎo)致的用戶態(tài)與內(nèi)核態(tài)的切換問題;其次如果有多個線程競爭鎖資源,鎖將會升級為輕量級鎖,它適用于在短時間內(nèi)持有鎖,且分鎖有交替切換的場景;輕量級鎖還使用了自旋鎖來避免線程用戶態(tài)與內(nèi)核態(tài)的頻繁切換,大大地提高了系統(tǒng)性能;但如果鎖競爭太激烈了,那么同步鎖將會升級為重量級鎖。減少鎖競爭,是優(yōu)化 Synchronized 同步鎖的關(guān)鍵。我們應(yīng)該盡量使 Synchronized 同步鎖處于輕量級鎖或偏向鎖,這樣才能提高 Synchronized 同步鎖的性能;通過減小鎖粒度來降低鎖競爭也是一種最常用的優(yōu)化方法;另外我們還可以通過減少鎖的持有時間來提高 Synchronized 同步鎖在自旋時獲取鎖資源的成功率,避免 Synchronized 同步鎖升級為重量級鎖。
以上就是深入剖析Java中的synchronized關(guān)鍵字的詳細(xì)內(nèi)容,更多關(guān)于Java synchronized關(guān)鍵字的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Maven腳手架如何基于jeecg實現(xiàn)快速開發(fā)
這篇文章主要介紹了Maven腳手架如何基于jeecg實現(xiàn)快速開發(fā),文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下2020-10-10SpringCloud openfeign相互調(diào)用實現(xiàn)方法介紹
在springcloud中,openfeign是取代了feign作為負(fù)載均衡組件的,feign最早是netflix提供的,他是一個輕量級的支持RESTful的http服務(wù)調(diào)用框架,內(nèi)置了ribbon,而ribbon可以提供負(fù)載均衡機(jī)制,因此feign可以作為一個負(fù)載均衡的遠(yuǎn)程服務(wù)調(diào)用框架使用2022-11-11java實現(xiàn)學(xué)生成績錄入系統(tǒng)
這篇文章主要為大家詳細(xì)介紹了java實現(xiàn)學(xué)生成績錄入系統(tǒng),具有一定的參考價值,感興趣的小伙伴們可以參考一下2018-01-01