Java并發(fā)編程中的synchronized關(guān)鍵字詳細解讀
前言
在 Java 早期版本中,synchronized 屬于 重量級鎖,效率低下。這是因為監(jiān)視器鎖(monitor)是依賴于底層的操作系統(tǒng)的 Mutex Lock 來實現(xiàn)的,Java 的線程是映射到操作系統(tǒng)的原生線程之上的。如果要掛起或者喚醒一個線程,都需要操作系統(tǒng)幫忙完成,而操作系統(tǒng)實現(xiàn)線程之間的切換時需要從用戶態(tài)轉(zhuǎn)換到內(nèi)核態(tài),這個狀態(tài)之間的轉(zhuǎn)換需要相對比較長的時間,時間成本相對較高。在 Java 6 之后, synchronized 引入了大量的優(yōu)化如自旋鎖、適應(yīng)性自旋鎖、鎖消除、鎖粗化、偏向鎖、輕量級鎖等技術(shù)來減少鎖操作的開銷,這些優(yōu)化讓 synchronized 鎖的效率提升了很多。
一、synchronized的使用方法
synchronized 塊是 Java 提供的一種原子性內(nèi)置鎖,Java 中的每個對象都可以把它當(dāng)作一個同步鎖來使用。
synchronized關(guān)鍵字可以用來修飾實例方法、靜態(tài)方法、代碼塊,表示對其進行加鎖,當(dāng)線程進入 synchronized 代碼塊前只有獲取到相應(yīng)的鎖才能訪問,否則自動進入自旋或阻塞狀態(tài)(BLOCKED)等待鎖被其他線程釋放后競爭鎖。
1、修飾實例方法 (鎖當(dāng)前對象實例)
給當(dāng)前對象實例加鎖,進入同步代碼前要獲得 當(dāng)前對象實例的鎖 。
synchronized void method() { //業(yè)務(wù)代碼 }
2、修飾靜態(tài)方法 (鎖類對象)
給當(dāng)前類加鎖,進入同步代碼前要獲得 當(dāng)前類class對象的鎖。這是因為靜態(tài)成員不屬于任何一個實例對象,歸整個類所有,不依賴于類的特定實例。
synchronized static void method() { //業(yè)務(wù)代碼 }
3、修飾代碼塊 (鎖指定對象/類對象)
- synchronized(object) 表示進入同步代碼前要獲得 給定對象的鎖。
- synchronized(類.class) 表示進入同步代碼前要獲得 給定 Class對象 的鎖。
synchronized(this) { //業(yè)務(wù)代碼 }
二、synchronized的特性
1. 可重入鎖
持有鎖的線程可直接進入此鎖關(guān)聯(lián)的任意其他代碼。
2. 非公平鎖
不是按照先來后到的原則來分配鎖。
3. 不可中斷鎖
synchronized在鎖競爭時是不可中斷的,獲取不到鎖的線程會一直處于阻塞狀態(tài)。而ReentrantLock獲取鎖失敗可以被interrupt()進行中斷操作。
三、synchronized相關(guān)問題
1. volatile和synchronized的區(qū)別是什么?
- volatile 關(guān)鍵字用于修飾變量,可保證變量的可見性和有序性。
- synchronized關(guān)鍵字用于修飾方法或代碼塊,可保證代碼塊的原子性以及代碼塊內(nèi)變量的可見性,以及代碼塊外部和內(nèi)部之間的有序性(代碼塊內(nèi)部的有序性不保證,例如DCL單例指令重排問題)。
2. 占有鎖的線程在什么情況下會釋放鎖?
- 占有鎖的線程執(zhí)行完了該代碼塊,然后釋放對鎖的占有;
- 占有鎖線程執(zhí)行發(fā)生異常,此時JVM會讓線程自動釋放鎖;
- 占有鎖線程進入 WAITING 狀態(tài)從而釋放鎖,例如在該線程中調(diào)用wait()方法等
四、synchronized的底層原理
對于synchronized同步代碼塊,編譯后在代碼塊前后分別有一個monitorenter 和 monitorexit 指令,在JVM中當(dāng)線程執(zhí)行到monitorenter指令時嘗試獲取指定對象的鎖,執(zhí)行到monitorexit 指令則釋放鎖。
對于synchronized同步方法,編譯后方法中有一個ACC_SYNCHRONIZED標識,在JVM中當(dāng)線程執(zhí)行到有此標識的方法時會隱式調(diào)用monitorenter和monitorexit。在執(zhí)行同步方法前會調(diào)用monitorenter,在執(zhí)行完同步方法后會調(diào)用monitorexit。最后都能達到加鎖的效果。
五、synchronized的鎖升級過程
早期synchronized實現(xiàn)的同步鎖為重量級鎖。但是重量級鎖會造成線程阻塞排隊,阻塞和喚醒線程會使CPU在用戶態(tài)和核心態(tài)之間頻繁切換,所以代價高、效率低。因此 Java6 對 synchronized 鎖進行了優(yōu)化,增加了輕量級鎖和偏向鎖。為了提高效率,不會一開始就使用重量級鎖,JVM在內(nèi)部會根據(jù)需要,按如下步驟進行鎖的升級:
1. 無鎖狀態(tài)
初期鎖對象剛創(chuàng)建時,還沒有任何線程來競爭,對象的markword是上圖的第一種情形,這偏向鎖標識位是0,鎖狀態(tài)01,說明該對象處于無鎖狀態(tài)(無線程競爭它)。
2. 偏向鎖
當(dāng)有一個線程來競爭鎖時,先用偏向鎖,會在對象頭的markword中記錄線程threadID,并且線程退出synchronized塊后偏向鎖不會主動釋放,因此之后此線程需要再次獲取鎖的時候,通過比較當(dāng)前線程的 threadID 和 對象頭中的threadID 發(fā)現(xiàn)一致,就不需要再做任何檢查和切換直接進入,這種競爭不激烈的情況下,效率非常高。如上圖第二種情形。
JDK 15中的偏向鎖 偏向鎖在單線程反復(fù)獲取鎖的場景下性能很高,但細想便知生產(chǎn)環(huán)境中高并發(fā)的場景下很難有這種場景。 而且對于偏向鎖來說,在多線程競爭時的撤銷操作十分復(fù)雜且?guī)砹祟~外的性能消耗(需要等到safe point,并STW)。 JDK 15 之前,偏向鎖默認是 開啟的,從 JDK 15 開始,默認就是關(guān)閉的了,需要顯式打開(-XX:+UseBiasedLocking)。
3. 輕量級鎖
當(dāng)需要獲取對象的hashcode值時就會禁用偏向鎖升級為輕量級鎖,將hashcode值寫入markword;或者當(dāng)有第二個線程開始競爭這個鎖對象,通過對比markword中記錄線程threadID發(fā)現(xiàn)不一致,那么首先需要查看Java 對象頭中記錄的線程 1 是否存活(偏向鎖不會主動釋放鎖),如果沒有存活,那么鎖對象被重置為無鎖狀態(tài),其它線程(線程 2)可以競爭將其設(shè)置為偏向鎖;如果存活,那么立刻查找該線程(線程 1)的棧幀信息,如果還是需要繼續(xù)持有這個鎖對象,那么暫停當(dāng)前線程 1,撤銷偏向鎖,升級為 輕量級鎖,如果線程 1 不再使用該鎖對象,那么將鎖對象狀態(tài)設(shè)為無鎖狀態(tài),重新偏向新的線程。如上圖第三種情形。
4. 重量級鎖(monitor)
當(dāng)輕量級鎖等待獲取鎖的線程自旋到達一定次數(shù),或者競爭的這個鎖對象的線程更多,導(dǎo)致了更多的切換和等待,JVM會把該鎖對象的鎖升級為重量級鎖。synchronized的重量級鎖是基于在監(jiān)視器(monitor)實現(xiàn)的,JVM中每個對象都可以關(guān)聯(lián)一個ObjectMonitor監(jiān)視器對象(C++實現(xiàn)),升級為重量級鎖后對象的Mark Word再次發(fā)生變化,會指向?qū)ο箨P(guān)聯(lián)的監(jiān)視器對象(如上圖第四種情形)。
ObjectMonitor的主要數(shù)據(jù)結(jié)構(gòu)如下:
ObjectMonitor() { _header = NULL; _count = 0; _waiters = 0, _recursions = 0; //線程重入次數(shù) _object = NULL; //指向?qū)?yīng)的java對象 _owner = NULL; //持有鎖的線程 _WaitSet = NULL; //等待隊列:處于wait狀態(tài)的線程會被加入到這個隊列中 _WaitSetLock = 0 ; _Responsible = NULL ; _succ = NULL ; _cxq = NULL ; //要競爭鎖的線程會先被加入到這個隊列中 FreeNext = NULL ; _EntryList = NULL ; //處于blocked阻塞狀態(tài)的線程,會被加入到這個隊列中 _SpinFreq = 0 ; _SpinClock = 0 ; OwnerIsThread = 0 ; _previous_owner_tid = 0; }
鎖競爭機制:
- 當(dāng)線程要獲取鎖時,首先將其加入cxq隊列的頭部,獲取失敗被阻塞后則加入EntryList隊列。
- 當(dāng)持有鎖的線程釋放鎖時,會根據(jù)策略喚醒cxq或者EntryList隊列中的線程來競爭鎖。
- 當(dāng)線程調(diào)用wait()方法后會釋放鎖進入阻塞狀態(tài)并加入waitSet等待隊列。當(dāng)調(diào)用notify方法后,會從waitSet中喚醒線程加入到cxq或者EntryList隊列。
六、synchronized的其它優(yōu)化
1. 鎖粗化
原則上,我們在編寫代碼的時候,總是推薦將同步塊的作用范圍限制得盡量小,只在共享數(shù)據(jù)的實際作用域中才進行同步,這樣是為了使得需要同步的操作數(shù)量盡可能變小,如果存在鎖競爭,那等待鎖的線程也能盡快拿到鎖。大部分情況下,上面的原則都是正確的,但是如果一系列的連續(xù)操作都對同一個對象反復(fù)加鎖和解鎖,甚至加鎖操作是出現(xiàn)在循環(huán)體中的,那即使沒有線程競爭,頻繁地進行互斥同步操作也會導(dǎo)致不必要的性能損耗。
public void test() { for (int i = 0; i < 100; i++) { synchronized (object) { i++; } } }
我們看以上方法,在for循環(huán)中,synchronized保證每個i++操作都是原子性的。但是以上的方法有個問題,就是在每次循環(huán)都會加鎖,開銷大,效率低。
虛擬機即時編譯器(JIT)在運行時,會自動根據(jù)synchronized的影響范圍進行鎖粗化優(yōu)化。
優(yōu)化后代碼:
public void test() { synchronized (object) { for (int i = 0; i < 100; i++) { i++; } } }
2. 鎖消除
消除鎖是虛擬機另外一種鎖的優(yōu)化,這種優(yōu)化更徹底,Java 虛擬機在 JIT 編譯時通過對運行上下文的掃描,去除不可能存在共享資源競爭的鎖,通過這種方式消除沒有必要的鎖,可以節(jié)省毫無意義的請求鎖時間,我們知道StringBuffer 是線程安全的,里面包含鎖的存在,但是如果我們在函數(shù)內(nèi)部使用 StringBuffer局部變量,那么代碼會在 JIT 后會自動將鎖消除。
到此這篇關(guān)于Java并發(fā)編程中的synchronized關(guān)鍵字詳細解讀的文章就介紹到這了,更多相關(guān)Java的synchronized關(guān)鍵字內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Java中使用Jedis操作Redis的實現(xiàn)代碼
本篇文章主要介紹了Java中使用Jedis操作Redis的實現(xiàn)代碼。詳細的介紹了Redis的安裝和在java中的操作,具有一定的參考價值,有興趣的可以了解一下2017-05-05