深入理解Synchronized
前言
Synchronized想必大家在工作中一定有接觸過,它算是Java并發(fā)場(chǎng)景下實(shí)現(xiàn)多線程安全一種比較直接的操作。有人會(huì)說它慢,確實(shí)。在JDK1.6之前,它有另一個(gè)名稱叫做:重量級(jí)鎖。但是從1.6版本起,它就在不斷被優(yōu)化?,F(xiàn)如今已經(jīng)是很成熟的并發(fā)安全技術(shù);所以關(guān)于Synchronized的考察也常常成為面試官青睞的話題。
本文我們會(huì)使用圖解的方式解析Synchronized的使用和原理,讓我們開始吧~

對(duì)象鎖和類鎖
什么是Synchronized?Synchronized是Java中的一個(gè)關(guān)鍵字,中文被稱為“同步鎖”。顧名思義,它是一種鎖,當(dāng)某一時(shí)刻有多個(gè)線程對(duì)同一段程序進(jìn)行操作時(shí),能夠保證只有一個(gè)線程能夠獲取到資源,因此保證了線程安全。
Synchronized主要有三種使用方式:
修飾普通方法,鎖作用于當(dāng)前對(duì)象實(shí)例。
修飾靜態(tài)方法,鎖作用于類的Class實(shí)例。
修飾代碼塊,作用于當(dāng)前對(duì)象實(shí)例,需要指定加鎖對(duì)象。
普通方法
Synchronized是一個(gè)關(guān)鍵字,當(dāng)作用于一個(gè)普通方法的時(shí)候,這個(gè)方法便被加上了同步鎖,意味著某一時(shí)刻只有一個(gè)線程可以操作訪問這個(gè)方法:
public class fancySyncTest {
public synchronized void method1(){
try {
System.out.println(Thread.currentThread().getName());
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] agrs) throws InterruptedException {
final fancySyncTest fs = new fancySyncTest();
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
fs.method1();
}
},"線程1獲取到資源");
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
fs.method1();
}
},"線程2獲取到資源");
t1.start();
t2.start();
}
}
這段代碼有方法method1( ),它的功能就是打印出當(dāng)前執(zhí)行這段代碼的線程,并且讓它休眠5秒鐘。然后我們開啟兩個(gè)線程,線程t1和t2,它們兩個(gè)一起啟動(dòng)。
這段代碼的執(zhí)行順序很簡(jiǎn)單,線程1或者線程2任意一個(gè)線程先去執(zhí)行method1( )的內(nèi)容,然后休眠5秒鐘。完事就釋放鎖,下一個(gè)線程會(huì)繼續(xù)執(zhí)行:

整個(gè)過程由于只有一個(gè)線程獲取到這個(gè)方法,去執(zhí)行方法里的內(nèi)容,所以method1( )里的代碼資源是線程安全的。
于是,當(dāng)前哪個(gè)對(duì)象調(diào)用了這個(gè)方法,那么當(dāng)前這段線程在執(zhí)行的時(shí)候就讓這個(gè)對(duì)象去訪問這個(gè)方法。比如我是讓對(duì)象fs去調(diào)用method1( ),那么這把鎖就作用于當(dāng)前的fs對(duì)象實(shí)例:

靜態(tài)方法
靜態(tài)方法和普通方法的區(qū)別只有一個(gè),就是Synchronized關(guān)鍵字是作用于靜態(tài)方法的。但是僅僅這個(gè)區(qū)別,代表著鎖的對(duì)象也是不同的。原因在于Java的靜態(tài)關(guān)鍵字它和實(shí)例對(duì)象沒有任何關(guān)系,它作用的資源是在類的初始化時(shí)期就加載的,所以它只能由每個(gè)類唯一的Class對(duì)象調(diào)用。當(dāng)它作用于一個(gè)Class對(duì)象時(shí),它就會(huì)將這一整個(gè)類都鎖住,簡(jiǎn)稱"類鎖":
public class fancySyncTest {
public static synchronized void method1(){
try {
System.out.println(Thread.currentThread().getName());
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] agrs) throws InterruptedException {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
fancySyncTest.method1();
}
},"線程1獲取到資源");
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
fancySyncTest.method1();
}
},"線程2獲取到資源");
t1.start();
t2.start();
}
}
可以看出,這段由靜態(tài)關(guān)鍵字修飾的代碼和普通的方法沒什么區(qū)別,唯一的區(qū)別就在于:由于method1( )是靜態(tài)的,所有我們不用創(chuàng)建對(duì)象,可以直接由類實(shí)例fancySynTest直接調(diào)用!作為代價(jià),這把鎖鎖住的對(duì)象也是直接覆蓋了這個(gè)類。也就是說,當(dāng)線程1執(zhí)行的時(shí)候,沒有別的線程可以訪問這個(gè)fancySyncTest類:

Synchronized修飾普通方法和靜態(tài)方法的區(qū)別只有一個(gè):粒度不同。類似于數(shù)據(jù)庫(kù)中表鎖和行鎖的區(qū)別。
代碼塊
Synchronized可以鎖住普通方法,也可以鎖住一個(gè)類,那么它鎖的粒度能否更小呢?是的,它還能鎖住一段簡(jiǎn)易的代碼塊。那么Synchronized如何定義一段代碼塊呢?其實(shí)定義一下作用的對(duì)象,然后將代碼用括號(hào){ }包裹起來(lái)就可以了:
public class fancySyncTest {
public synchronized void method1(){
synchronized (this) {
// 邏輯代碼
}
}
}
代碼塊鎖住的對(duì)象就是后面括號(hào)里的東西。比如這里的synchronized (this),意味著只有當(dāng)前對(duì)象才可以訪問這段代碼塊,你也可以定義為其它對(duì)象。
Synchronized原理
其實(shí),Synchronized只是Java中的一個(gè)關(guān)鍵字,那么它底層是如何真正意義地實(shí)現(xiàn)鎖呢?答案就是monitor監(jiān)視器鎖。無(wú)論是synchronized代碼塊還是synchronized方法,其線程安全的語(yǔ)義實(shí)現(xiàn)最終依賴的都是monitor,它才是真正意義上的鎖。
為了得到Synchronized的底層代碼,我們先寫一段簡(jiǎn)單demo:
public class fancySynchronizedTest {
public synchronized void method1() {
}
public void method2() {
synchronized (this){
}
}
public static void main(String[] args) {
fancySynchronizedTest test = new fancySynchronizedTest();
test.method1();
test.method2();
}
}
然后對(duì)它使用java-c 反編譯,得到以下文件:
public class fancySynchronizedTest {
public fancySynchronizedTest();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."":()V
4: return
public synchronized void test1();
Code:
0: return
public void test2();
Code:
0: aload_0
1: dup
2: astore_1
3: monitorenter //monitor加鎖
4: aload_1
5: monitorexit //monitor解鎖
6: goto 14
9: astore_2
10: aload_1
11: monitorexit //monitor異步退出解鎖
12: aload_2
13: athrow
14: return
Exception table:
from to target type
4 6 9 any
9 12 9 any
public static void main(java.lang.String[]);
Code:
0: new #2 // class SynchronizedTest
3: dup
4: invokespecial #3 // Method "":()V
7: astore_1
8: aload_1
9: invokevirtual #4 // Method test1:()V
12: aload_1
13: invokevirtual #5 // Method test2:()V
16: return
}
可以看到,test2( )在加上了synchronized同步代碼塊后,會(huì)輸出以下指令:monitorenter和monitorexit。
monitorenter存在于同步代碼塊開始的位置,而monitorexit存在于同步代碼塊結(jié)束的位置,它們分別代表著獲取鎖和釋放鎖。每一個(gè)monitorenter都必須對(duì)應(yīng)一個(gè)monitorexit。并且,每一個(gè)對(duì)象在其堆內(nèi)存的數(shù)據(jù)結(jié)構(gòu)中,它的對(duì)象頭都會(huì)關(guān)聯(lián)一個(gè)完整的monitor結(jié)構(gòu)。
為了讓大家更熟悉monitor,我們帶大家來(lái)閱讀一下monitor的源碼。
對(duì)于我們通常使用的HotSpot虛擬機(jī),monitor是由ObjectMonitor實(shí)現(xiàn)的。其源碼是用c++來(lái)實(shí)現(xiàn)的,位于HotSpot虛擬機(jī)源碼ObjectMonitor.hpp文件中,我們可以在社區(qū)版本的JDK上閱讀到,因?yàn)樗情_源的:

注:該源碼版本為jdk8u的社區(qū)版open jdk
我們需要關(guān)注該圖上的三個(gè)變量:_owner、_WaitSet、_EntryList ,因?yàn)閙onitor就是通過它們,來(lái)實(shí)現(xiàn)上鎖與釋放鎖的。我們先來(lái)看看這三個(gè)變量的定義:
_EntryList:

它的定義為:阻塞在路口處的線程的集合
再來(lái)看看_WaitSet:

它的定義為:處于等待監(jiān)視器的線程的集合
最后看看變量_owner:

它的意思是指向持有鎖的線程。
所以,它的完整過程就是如下所示:

monitor自身會(huì)設(shè)置一個(gè)變量count來(lái)作為計(jì)數(shù)器維護(hù)這把可重入鎖,當(dāng)沒有線程獲取這把鎖的時(shí)候,它的值為0,如果有一個(gè)線程獲取這把鎖,它的值就會(huì)+1,并且設(shè)置該線程為鎖的持有者。_owner指向的就是當(dāng)前持鎖線程。如果該線程已經(jīng)占用該鎖,并且重新進(jìn)入,那么count的值就會(huì)+1。當(dāng)執(zhí)行到monitorexit的時(shí)候,count的值就會(huì)-1,直到monitor值為0的時(shí)候,該持鎖線程會(huì)進(jìn)入到WaitSet里面,將狀態(tài)改為等待狀態(tài),讓其他處于EntryList里的阻塞線程重新自旋獲取這把鎖。
以上就是完整的monitor獲取鎖和釋放鎖的過程。不知道你是否會(huì)覺得它和AQS的原理很像?其實(shí)這些底層架構(gòu)設(shè)計(jì)的思想都是相通的。
鎖的優(yōu)化
可能是意識(shí)到Synchronized作為重量級(jí)鎖性能上的不足,從jdk1.6開始Java團(tuán)隊(duì)就對(duì)它進(jìn)行了優(yōu)化。通過各種各樣的手段,如自旋鎖、偏向鎖、輕量級(jí)鎖等技術(shù)來(lái)減少鎖的開銷,那么我們就來(lái)解釋一下這些鎖操作以及升級(jí)過程。
自旋鎖
首先聊聊自旋鎖,自旋鎖顧名思義就是自旋。當(dāng)一個(gè)線程在獲取鎖的時(shí)候,如果該鎖已被其它線程獲取到,那么該線程就會(huì)去循環(huán)自旋獲取鎖,不停地判斷該鎖是否能夠已經(jīng)被釋放,自選直到獲取到鎖才會(huì)退出循環(huán)。通常該自選在源碼中都是通過for(; ;)或者while(true)這樣的操作實(shí)現(xiàn),非常粗暴。
那么都說是自旋了,自旋就代表著占用cpu資源,使用自旋鎖的目的是為了避免線程在阻塞和喚醒狀態(tài)之間切換而占用資源,畢竟線程從阻塞狀態(tài)切換到喚醒狀態(tài)需要CPU從用戶態(tài)轉(zhuǎn)化為內(nèi)核態(tài),而頻繁的狀態(tài)切換就會(huì)導(dǎo)致CPU的資源浪費(fèi),所以引入了自選鎖。但是自旋鎖也必定要設(shè)置一個(gè)自旋的閾值,否則因?yàn)樽孕L(zhǎng)期占用CPU核心數(shù),也是一種資源的浪費(fèi)。在JDK1.6中默認(rèn)的自旋次數(shù)為10次,也就是說如果一個(gè)線程自旋超過10次,那么它就會(huì)自動(dòng)進(jìn)入掛起狀態(tài)從而節(jié)約資源。以下為自旋鎖獲取鎖過程:

自旋鎖在JDK源碼中有大量的應(yīng)用,之后我們還會(huì)接觸到。
鎖消除
鎖消除指的是虛擬機(jī)即時(shí)編譯器在運(yùn)行的時(shí),對(duì)一些代碼上要求同步,但是被檢測(cè)到不可能存在共享數(shù)據(jù)競(jìng)爭(zhēng)的鎖進(jìn)行削除。那么如何確定這個(gè)鎖是否需要進(jìn)行削除?主要來(lái)源于逃逸分析的判斷,如果判斷到一段代碼中,在堆上的所有數(shù)據(jù)都不會(huì)逃逸出去被其他線程訪問到,那就可以把它們當(dāng)作棧上數(shù)據(jù)對(duì)待,認(rèn)為它們是線程私有的,同步加鎖自然就無(wú)須進(jìn)行。舉個(gè)例子:
public void method() {
synchronized (new Object()) {
//代碼邏輯
}
}
這段代碼里面,我們new了一個(gè)Object對(duì)象來(lái)作為鎖對(duì)象,但是這個(gè)對(duì)象也只有在method( )中被使用,其完整的生命周期都在這個(gè)方法中,也就是說JVM在經(jīng)過逃逸分析后會(huì)對(duì)它進(jìn)行棧上分配,由于在底層變成了私有數(shù)據(jù),那么也就無(wú)需加鎖了。其被優(yōu)化后的代碼會(huì)變成:
public void method() {
//代碼邏輯
}
這就是鎖的消除,JVM虛擬機(jī)在判斷一段代碼沒必要加鎖后,會(huì)消除該鎖的存在。
那么我們?cè)賮?lái)看看什么是鎖的粗化。
鎖粗化
鎖粗化指的是在JIT編譯時(shí),發(fā)現(xiàn)如果有一段代碼中頻繁的加鎖釋放鎖,會(huì)將前后的鎖合并為一個(gè)鎖,避免頻繁加鎖釋放鎖。舉個(gè)例子:
public void method() {
for(int i = 0;i < 100; i++) {
synchronized (new Object()) {
//代碼邏輯
}
}
}
大伙說,如果按照正常的synchronized步驟走,這個(gè)循環(huán)需要進(jìn)行多少次的加鎖解鎖操作,這性能可想而知!
leader看了你這段代碼估計(jì)想罵娘。。。
好在現(xiàn)在的JVM已經(jīng)優(yōu)化的足夠好了。當(dāng)你這段代碼在即時(shí)編譯時(shí),JVM檢測(cè)到你每一次都是對(duì)同一個(gè)對(duì)象加鎖,那么就會(huì)把這一串連續(xù)頻繁的加鎖解鎖操作優(yōu)化成僅僅一次公共的加鎖解鎖操作,這樣性能就提升了很多了:
public void method() {
synchronized (new Object()) {
for(int i = 0;i < 100; i++) {
//代碼邏輯
}
}
}
所以,所謂的鎖粗化,就是通過增加鎖的范圍,減少鎖的數(shù)量來(lái)減少開銷。
偏向鎖
在提偏向鎖之前,我們需要先提一下一個(gè)Java對(duì)象在內(nèi)存中是如何存儲(chǔ)的。
一個(gè)對(duì)象在堆內(nèi)存中由三部分區(qū)域組成,分別為:對(duì)象頭(Header)、實(shí)例數(shù)據(jù)(Instance Data)和對(duì)其填充(Padding):

其中對(duì)象頭包括了兩部分,一部分是關(guān)于堆對(duì)象的布局、類型、GC狀態(tài)、同步狀態(tài)和標(biāo)識(shí)哈希碼的基本信息,它被稱為"Mark Word "。
另一部分是類型指針,是對(duì)象指向它的類元數(shù)據(jù)的指針,虛擬機(jī)通過這個(gè)指針來(lái)確定這個(gè)對(duì)象是哪個(gè)類的實(shí)例。

那么我們今天的重點(diǎn)在于Mark Word,其結(jié)構(gòu)如下:

好了,在知道了對(duì)象頭的重要結(jié)構(gòu)Mark Word,我們就繼續(xù)聊聊偏向鎖。
偏向鎖是JVM認(rèn)為沒有發(fā)生并發(fā)的場(chǎng)景下提供的鎖。它是JDK 1.6中的重要引進(jìn),因?yàn)镠otSpot團(tuán)隊(duì)發(fā)現(xiàn)在大多數(shù)情況下,鎖不僅不存在多線程競(jìng)爭(zhēng),而且總是由同一線程多次獲得,為了讓線程獲得鎖的代價(jià)更低,引進(jìn)了偏向鎖。
所以這就是它為什么叫“偏向鎖”?!捌?,就是偏心的“偏”、偏袒的“偏”,它的意思是這個(gè)鎖會(huì)偏向于第一個(gè)獲得它的線程,會(huì)在對(duì)象頭存儲(chǔ)鎖偏向的線程ID,以后該線程進(jìn)入和退出同步區(qū)域時(shí)只需要檢查是否為偏向鎖,即鎖標(biāo)志位以及ThreadID即可:

輕量級(jí)鎖
輕量級(jí)鎖是JDK 1.6之中加入的新型鎖機(jī)制,它名字中的“輕量級(jí)”是相對(duì)于使用monitor的傳統(tǒng)鎖而言的,因此傳統(tǒng)的鎖機(jī)制就稱為“重量級(jí)”鎖。
首先需要強(qiáng)調(diào)一點(diǎn)的是,輕量級(jí)鎖并不是用來(lái)代替重量級(jí)鎖的。引入輕量級(jí)鎖的目的在于:在多線程交替執(zhí)行同步塊的情況下,盡量避免重量級(jí)鎖引起的性能消耗,但是如果多個(gè)線程在同一時(shí)刻進(jìn)入臨界區(qū),會(huì)導(dǎo)致輕量級(jí)鎖膨脹升級(jí)重量級(jí)鎖,所以輕量級(jí)鎖的出現(xiàn)并非是要替代重量級(jí)鎖。當(dāng)該鎖為輕量級(jí)鎖時(shí),其Mark Word的狀態(tài)變化如下:

重量級(jí)鎖
在JDK1.6之前,Synchronized就是一把“重量級(jí)鎖”。它需要依賴操作系統(tǒng)級(jí)別的mutex和condition variable來(lái)實(shí)現(xiàn)。重量級(jí)鎖會(huì)讓搶占鎖的線程從用戶態(tài)轉(zhuǎn)變?yōu)閮?nèi)核態(tài),所以開銷很大:

鎖升級(jí)
鎖升級(jí),顧名思義就是鎖的等級(jí)不斷上升。因?yàn)殒i是會(huì)消耗性能的,所以鎖不斷升級(jí),它的性能就會(huì)越差。當(dāng)然這一切都是為了滿足安全、復(fù)雜的業(yè)務(wù)場(chǎng)景。
并且鎖只會(huì)升級(jí),不會(huì)降級(jí):

總結(jié)
本文我們?cè)敿?xì)地介紹了Synchronized的三種使用方式、它作為鎖的特性、詳細(xì)的原理以及七種鎖的優(yōu)化過程。Synchronized作為虛擬機(jī)級(jí)別的鎖,無(wú)論是業(yè)務(wù)的使用還是面試,都是經(jīng)常被照顧的對(duì)象,所以搞懂它就很重要。下一次我們會(huì)講一講它異父異母的親兄弟:JDK級(jí)別的鎖Lock,以及談?wù)勊鼈兪侨绾谓?jīng)常拿出來(lái)被比較的~
如有文章對(duì)你有幫助,
“在看”和轉(zhuǎn)發(fā)是對(duì)我最大的支持!
推薦:
點(diǎn)擊領(lǐng)取:151個(gè)大廠面試講解?。▓D片可上下滑動(dòng)?。??


