面試官最想要的synchronized,你值得擁有
synchronized簡介
synchronized
是Java
語言的一個關(guān)鍵字,它本身的意思為同步,是用來保證線程安全的,可用來給對象和方法或者代碼塊加鎖,當(dāng)它鎖定一個方法或者一個代碼塊的時候,同一時刻最多只有一個線程執(zhí)行這段代碼。
synchronized
一句話來解釋其作用就是:能夠保證同一時刻最多只有一個線程執(zhí)行該段代碼,以達(dá)到并發(fā)安全的效果。synchronized
就猶如一把鎖,當(dāng)一個線程獲取到該鎖,別的線程只能等待其執(zhí)行完才能執(zhí)行。
synchronized
可以說是Java中元老級的關(guān)鍵字了,也是面試的高頻的問點,在jdk1.6
之前它是一把重量級鎖,性能不被大家看好,在次之后對它做了很多優(yōu)化,性能也大大提升。
那么synchronized
的實現(xiàn)的底層原理是什么,jdk1.6
之后又對它做了哪些優(yōu)化呢?接下來我們一步一步的分析。
synchronized的特性
synchronized
能夠保證在多線程的情況下線程安全,直接可以它的特性進行總結(jié)原因,synchronized
有以下四個特性:
-
原子性:保證被 synchronized
修飾的一個或者多個操作,在執(zhí)行的過程中不會被任何的因素打斷,即所謂的 原子操作,直到鎖被釋放。 -
可見性:保證持有鎖的當(dāng)前線程在釋放鎖之前,對共享變量的修改會刷新到主存中,并對其它線程可見。 -
有序性:保證多線程時刻中只有一個線程執(zhí)行,線程執(zhí)行的順序都是有序的。 -
可重入性:保證在多線程中,有其他的線程試圖競爭持有鎖的臨界資源時,其它的線程會處于等待狀態(tài),而當(dāng)前持有鎖的線程可以重復(fù)的申請自己持有鎖的臨界資源。
上面的也是粗略的進行概括,接下來就一步一步的進行深入的分析synchronized
的這四個特性的底層原理。
原子性
上面介紹了原子性就是一個或者多個操作,在執(zhí)行的過程中不會被任何的因素打斷,這里的任何因素打斷具體一點主要是指cpu的線程調(diào)度。
在Java語言中對基本數(shù)據(jù)類型讀取和賦值才是原子操作,這些操作在執(zhí)行的過程不會被中斷。而像a++
或者a+=1
類似的操作,都并非是原子性操作。
因為這些操作底層執(zhí)行的流程分為這三步:讀取值、計算值、賦值。才算完成上面的操作,在多線程的時候就會存在線程安全的問題,產(chǎn)生臟數(shù)據(jù),導(dǎo)致最后的結(jié)果并非預(yù)期的結(jié)果。
在面試的過程中也會有很多面試官常常拿volatile
和synchronized
做比較,在原子性方面區(qū)別就是volatile
沒有辦法保證原子性,而synchronized
可以實現(xiàn)原子性。
這里簡單的只對volatile
做一個簡介,volatile
的具體作用主要有兩個:保證可見性、禁止指令重排,這里畫了一個圖給大家,可以參考:
具體的volatile為什么沒辦法保證原子操作,我之前寫過一篇關(guān)于volatile詳細(xì)的文章,可以參考這一篇文章[]。
那么synchronized
的底層又是怎么實現(xiàn)原子性的呢?這里又要從synchronized
的字節(jié)碼說起,在idea中寫了一段簡單的代碼如下所示:
public class TestSynchronized implements Runnable {
@Override
public void run() {
synchronized (this) {
System.out.println("同步代碼塊");
}
}
public static void main(String[] args) {
TestSynchronized sync = new TestSynchronized();
Thread t = new Thread(sync);
t.start();
}
}
代碼很簡單,通過字節(jié)碼進行分析,執(zhí)行的字節(jié)碼如下圖所示,在字節(jié)碼中可以看出在執(zhí)行代碼塊中的代碼之前有一個monitorenter
,后面的是離開monitorexit
。
不難猜測執(zhí)行同步代碼塊中的代碼時,首先要獲取對象鎖,對應(yīng)使用monitorenter
指令 ,在執(zhí)行完代碼塊之后,就要釋放鎖,所對應(yīng)的指令就是monitorexit
。
在這里又會有一個面試考點就是:什么會出現(xiàn)兩次的monitorexit呢? 這是因為一個線程對一個對象上鎖了,后續(xù)就一定要解鎖,第二個monitorexit
是為了保證在線程異常時,也能正常解鎖,避免造成死鎖。
可見性
synchronized
實現(xiàn)可見性就是在解鎖之前,必須將工作內(nèi)存中的數(shù)據(jù)同步到主內(nèi)存,其它線程操作該變量時每次都可以看到被修改后的值。
說到工作內(nèi)存和主內(nèi)存這個要從JMM
說起,主存是放共享變量的地方,而工作內(nèi)存是線程私有的,存放的是主存的變量的副本,線程不會對主存的變量直接操作。這里畫了一張圖給大家理解:
具體講解JMM
的文章我之前寫過一篇詳細(xì)的文章,這里只做上面的概述,詳細(xì)了解JMM
的可以看這一篇[]。
有序性
synchronized
在實現(xiàn)有序性時,多線程并發(fā)訪問只有一個線程執(zhí)行,從而保證線程執(zhí)行的順序都是有序的。
synchronized
為了實現(xiàn)有序性,通過阻塞其它線程的方式,來達(dá)到線程的有序執(zhí)行,接下來看一個簡單的代碼:
public class TestSynchronized implements Runnable {
Object o= new Object();
public static void main(String[] args) throws InterruptedException {
TestSynchronized sync = new TestSynchronized ();
Thread t1 = new Thread(sync);
Thread t2 = new Thread(sync);
t1.start();
t2.start();
}
@Override
public void run() {
synchronized (o) {
try {
System.out.println(Thread.currentThread().getName() + "線程開始執(zhí)行");
Thread.sleep(5000);
System.out.println(Thread.currentThread().getName() + "線程等待5秒后執(zhí)行完畢");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
這個毋庸置疑,當(dāng)你加了synchronized
代碼塊的時候,這兩個線程執(zhí)行必須是有序的,同一個線程前后的輸出一定會在一起,執(zhí)行的結(jié)果如圖所示:
假如注釋掉synchronized
的代碼塊,兩個線程的執(zhí)行就不再是有序的執(zhí)行,就會出現(xiàn)如圖所示的情況:
可重入性
synchronized
的可重入性就是當(dāng)一個線程已經(jīng)持有鎖對象的臨界資源,當(dāng)該線程再次請求對象的臨界資源,可以請求成功,這種情況屬于重入鎖。
實現(xiàn)的底層原理就是synchronized
底層維護一個計數(shù)器,當(dāng)線程獲取該鎖時,計數(shù)器+1
,再次獲取鎖時繼續(xù)+1
,釋放鎖時,計數(shù)器-1,當(dāng)計數(shù)器值為0時,表明該鎖未被任何線程所持有,其它線程可以競爭獲取鎖。
synchronized基本用法
前面詳細(xì)的介紹了synchronized
的基本特性,接下來詳細(xì)的介紹synchronized
的基本用法,我們基本都知道大部分是時候只會用到同步方法上,但是它的用法有下面三種:
-
同步普通方法:在方法上添加synchronized關(guān)鍵字。 -
同步靜態(tài)方法:在方法上添加synchronized關(guān)鍵字,并且方法被static修飾。 -
同步代碼塊:執(zhí)行的代碼操作被synchronized修飾。
-
鎖定this實例或者實例對象 -
鎖定類字節(jié)碼
在同步方法中這個相信大家都是知道,代碼如下圖所示:
private synchronized void syncMethod() {
// 邏輯代碼
}
這里有一個問題就是對于synchronized
的鎖無非就是兩種,對于同步方法中的鎖對象又是什么呢? ,這里畫了一張圖給大家,如下如圖所示:
在同步普通方法中鎖對象就是this
,也就是當(dāng)前對象,哪個對象調(diào)用的同步方法,鎖對象就是就是它。
當(dāng)然同步普通方法只能作用在單例上,若不是單例,同步方法就會失效,原因很簡單,多例中鎖對象不一樣,沒辦法生效。
在同步靜態(tài)方法中的鎖對象是當(dāng)前類的class對象,這個相信大家都能想到。
在同步代碼塊中,可以有很多的玩法,因為鎖對象是任意的,由程序員自己操作指定,主要這幾種方式獲得鎖對象:this 、Object、this.getClass()、className.getClass()。
具體用哪種就要看你的具體的業(yè)務(wù)場景了,這里只是做了總結(jié)和歸納。
synchronized的優(yōu)化
在JVM
的書籍中介紹到,synchronized
在jdk6
之前一直使用的是重量級鎖,在jdk6
之后便對其進行了優(yōu)化,新增了偏向鎖、輕量級鎖(自旋鎖),并通過鎖消除、鎖粗化、自旋鎖、自適應(yīng)自旋等方法使用于各種場景,大大提升了synchronized
的性能。
下面就來詳細(xì)的介紹synchronized被優(yōu)化的過程以及原理,對synchronized優(yōu)化的實現(xiàn)的具體的原理圖如下所示:
在synchronized優(yōu)化的最重要的就是鎖升級的優(yōu)化過程,也是大廠面試的必問的鎖知識點,接下來我們就詳細(xì)的了解這個過程。
鎖升級
在講解鎖升級的過程,先了解對象的在內(nèi)存中的布局情況,為什么呢?因為鎖的信息是存儲在對象的markword
中,只有了解了對象的布局,對深入的了解鎖升級會更有幫助。
在我們創(chuàng)建一個對象后,大部分時候,對象都是分配在堆中,因為還有可能對象在棧上分配,所以這里用大部分情況。
對于一個對象創(chuàng)建完之后,在內(nèi)存中的布局情況,我之前也寫過一篇文章,詳細(xì)可以參考這一篇[],這里做一個大概的回顧,一個對象在內(nèi)存中的布局圖如下所示。
對象在內(nèi)存布局中主要分為以下三個部分:對象頭(markword、class pointer)、示例數(shù)據(jù)(instance data)、對齊(可有可無)。
其中對象頭中,若是對象為數(shù)組則還包含數(shù)據(jù)的長度,其中markword
中主要包含信息有:GC年齡信息、鎖對象信息、hashCode信息。
class pointer是類型指針,指向當(dāng)前對象class文件,實例數(shù)據(jù)若是一個對象有屬性private int n=1
,這是n=1
即使存儲在示例數(shù)據(jù)中。
最后的填充可有可無,這個取決于對象的大小,所示對象大小能被8字節(jié)整除,則該部分沒有,不能被整除,就會填充對象大小到能夠被8字節(jié)整除。
在對象的內(nèi)存布局中,最值得我們關(guān)注的就是markword
,因為markword
是存儲鎖信息的,接下來的實驗中,就是要觀察markword
包含的位里面的大小的變化。
要在實際中觀察到對象的內(nèi)存布局情況,可以借助JOL
依賴庫,全程是JAVA Objct Layout
,即是Java對象布局,只需要在你的maven工程里面引入如下maven坐標(biāo):
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.9</version>
</dependency>
然后創(chuàng)建一個SpringBoot項目,加入上面Maven依賴,接著創(chuàng)建Java類JaveObjectLayout
,代碼如下:
public class JaveObjectLayout {
public static void main(String[] args) {
Object o = new Object();
String s = ClassLayout.parseInstance(o).toPrintable();
System.out.println(s);
}
}
執(zhí)行代碼后輸出的結(jié)果如下圖所示:
有人問這是啥?不慌,且聽我慢慢道來,這個就是Java在內(nèi)存中的布局?jǐn)?shù)據(jù),前八個字節(jié)表示的是markword
,其中OFFSET
表示起始位,SIZE
表示偏移位。
比如第一行0 4
,表示第0個字節(jié)開始算4個字節(jié),然后第二行4 4
表示第4個字節(jié)開始算4個字節(jié),這樣就一共8個字節(jié),表示完整的markword信息。
其中后面的VAlUE
數(shù)據(jù)表示的是對應(yīng)的這4個字節(jié)上的具體位的數(shù)據(jù),1字節(jié)=8位,這個也剛好對應(yīng)。
在能看懂這個之前必須要了解各種鎖對應(yīng)的位數(shù)上的是0還是1,才能夠知道上面輸出的表示是什么信息,看一張各種鎖表示的信息圖:
其中無鎖狀態(tài)位001,偏向鎖為101,輕量級鎖為00,而重量級鎖為10,最后11表示GC信息。這個怎么對應(yīng)呢?我們再來看上面的那種圖:
從代碼中可以看出,是沒有加鎖的,所有對應(yīng)的最低三位為001為無鎖狀態(tài),當(dāng)代碼改成如下圖所示:
Object o = new Object();
synchronized (o) {
String s = ClassLayout.parseInstance(o).toPrintable();
System.out.println(s);
}
再次輸出,這時候便表示輕量級鎖,前四個字節(jié)的數(shù)據(jù)明顯變大,后面字節(jié)的數(shù)據(jù)都沒有變化,說明鎖信息是存儲在markword
中的,所謂的加鎖,就是在對象的markword中儲存鎖信息(包括線程的ThreadID
),并且對象的鎖狀態(tài)由0改為了1,表示該對象已經(jīng)被哪個線程所持有。
接下來我們來聊聊詳細(xì)的鎖升級的過程,當(dāng)初始化完對象后,對象處于無鎖狀態(tài),在只有一個線程第一次使用該對象,不存在鎖競爭時,我們便會認(rèn)為該線程偏向于它。
偏向鎖的實質(zhì)就是將線程的ThreadID
存儲于markword
中,表明該線程偏向于它。
若是某一時刻又來了線程二、線程三也想競爭這把鎖,此時是輕度的競爭,便升級為輕量級鎖,于是這三個線程就開始競爭了,他們就會去判斷鎖是否由釋放,若是沒有釋放,沒有獲得鎖的線程就會自旋,這就是自旋鎖。
在自旋的過程,也會嘗試的去獲取鎖,直到獲取鎖成功。在jdk1.6
之后又出現(xiàn)了自適應(yīng)自旋,就是jdk根據(jù)運行的情況和每個線程運行的情況決定要不要升級。
自適應(yīng)自旋是對自旋鎖優(yōu)化方式的進一步優(yōu)化,它的自旋的次數(shù)不再固定,其自旋的次數(shù)由前一次在同一個鎖上的自旋時間及鎖的擁有者的狀態(tài)來決定,這就解決了自旋鎖帶來的缺點。
這個競爭的過程的實質(zhì)就是看誰能把自己的ThreadID
貼在對象的markword
中,而這個過程就是CAS操作,原子操作。
倘若此時又來了線程四、線程5.....線程n,都想獲取該鎖,競爭越來越激烈了,此時就會升級為重量級鎖。
所謂的重量級鎖,為什么叫做重量級呢?因為重量級鎖要通過操作系統(tǒng),由用戶態(tài)切換到內(nèi)核態(tài)的過程,這個切換的過程是非常消耗資源的,并且經(jīng)過系統(tǒng)調(diào)用。
那么為啥重量級鎖那么消耗資源?還要它,要它有何用?是這樣的,假如沒有重量級鎖,不管有多少個線程都是自旋,那么當(dāng)線程是大了,等待的線程永遠(yuǎn)在自旋。
自旋是要消耗cpu資源的,這樣cpu就撐不住了,反而性能會大大下降,在經(jīng)過反復(fù)的測試后,肯定是有一個臨界值,當(dāng)超過這個臨界值時,反而使用重量級鎖性能更加高效。
因為重量級鎖不需要消耗cpu的資源,都把等待的線程放在了一個等待的隊列中,需要的時候在喚醒他們。
在jdk1.6
之前當(dāng)線程的自選次數(shù)超過10次或者等待的自旋的線程數(shù)超過了CPU核數(shù)的二分之一,就會升級為重量級鎖。
當(dāng)然也有情況就是偏向鎖一開始就重度競爭,這是就直接升級為重量級鎖,這個在互聯(lián)網(wǎng)項目中也是很常見的。
經(jīng)過上面的詳細(xì)講解于是就出現(xiàn)了下面的鎖升級圖,在不同的條件就會升級為不同的鎖:
鎖消除、鎖粗化
鎖消除是另一種鎖的優(yōu)化措施,在編譯期間,會對上下文進行掃描,去除掉不可能存在競爭的鎖,這樣就不必執(zhí)行沒有必要的上鎖和解鎖操作消耗性能。
鎖粗化就是擴大所得范圍,避免反復(fù)執(zhí)行加鎖和釋放鎖,避免不必要的性能消耗。
特別推薦一個分享架構(gòu)+算法的優(yōu)質(zhì)內(nèi)容,還沒關(guān)注的小伙伴,可以長按關(guān)注一下:
長按訂閱更多精彩▼
如有收獲,點個在看,誠摯感謝
免責(zé)聲明:本文內(nèi)容由21ic獲得授權(quán)后發(fā)布,版權(quán)歸原作者所有,本平臺僅提供信息存儲服務(wù)。文章僅代表作者個人觀點,不代表本平臺立場,如有問題,請聯(lián)系我們,謝謝!