synchronized 與多線程的哪些關(guān)系
synchronized 關(guān)鍵字
JVM 實現(xiàn)的 synchronized
JDK 實現(xiàn)的 ReentrantLock
synchronized關(guān)鍵字解決的是多個線程之間訪問資源的同步性,synchronized關(guān)鍵字可以保證被它修飾的方法或者代碼塊在任意時刻只能有一個線程執(zhí)行。
在 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 之后 Java 官方對從 JVM 層面對synchronized 較大優(yōu)化,JDK1.6對鎖的實現(xiàn)引入了大量的優(yōu)化,如自旋鎖、適應(yīng)性自旋鎖、鎖消除、鎖粗化、偏向鎖、輕量級鎖等技術(shù)來減少鎖操作的開銷。
主要的三種使用方式
修飾實例方法,作用于當(dāng)前對象實例加鎖,進(jìn)入同步代碼前要獲得當(dāng)前對象實例的鎖。
修飾靜態(tài)方法,作用于當(dāng)前類對象加鎖,進(jìn)入同步代碼前要獲得當(dāng)前類對象的鎖 。也就是給當(dāng)前類加鎖,會作用于類的所有對象實例,因為靜態(tài)成員不屬于任何一個實例對象,是類成員( static 表明這是該類的一個靜態(tài)資源,不管new了多少個對象,只有一份,所以對該類的所有對象都加了鎖)。所以如果一個線程A調(diào)用一個實例對象的非靜態(tài) synchronized 方法,而線程B需要調(diào)用這個實例對象所屬類的靜態(tài) synchronized 方法,是允許的,不會發(fā)生互斥現(xiàn)象,因為訪問靜態(tài) synchronized 方法占用的鎖是當(dāng)前類的鎖,而訪問非靜態(tài) synchronized 方法占用的鎖是當(dāng)前實例對象鎖。
修飾代碼塊,指定加鎖對象,對給定對象加鎖,進(jìn)入同步代碼庫前要獲得給定對象的鎖。和 synchronized 方法一樣,synchronized(this)代碼塊也是鎖定當(dāng)前對象的。synchronized 關(guān)鍵字加到 static 靜態(tài)方法和 synchronized(class)代碼塊上都是是給 Class 類上鎖。這里再提一下:synchronized關(guān)鍵字加到非 static 靜態(tài)方法上是給對象實例上鎖。另外需要注意的是:盡量不要使用 synchronized(String a) 因為JVM中,字符串常量池具有緩沖功能。
雙重校驗鎖實現(xiàn)對象單例(線程安全)
public class Singleton {private volatile static Singleton uniqueInstance;private Singleton() {}public static Singleton getUniqueInstance() {//先判斷對象是否已經(jīng)實例過,沒有實例化過才進(jìn)入加鎖代碼if (uniqueInstance == null) {//類對象加鎖synchronized (Singleton.class){if (uniqueInstance == null) {uniqueInstance = new Singleton();}}}return uniqueInstance;}}
另外,需要注意 uniqueInstance 采用 volatile 關(guān)鍵字修飾也是很有必要。uniqueInstance 采用 volatile 關(guān)鍵字修飾也是很有必要的,uniqueInstance = new Singleton() 這段代碼其實是分為三步執(zhí)行:
為 uniqueInstance 分配內(nèi)存空間
初始化 uniqueInstance
將 uniqueInstance 指向分配的內(nèi)存地址
但是由于 JVM 具有指令重排的特性,執(zhí)行順序有可能變成 1->3->2。指令重排在單線程環(huán)境下不會出先問題,但是在多線程環(huán)境下會導(dǎo)致一個線程獲得還沒有初始化的實例。例如,線程 T1 執(zhí)行了 1 和 3,此時 T2 調(diào)用 getUniqueInstance() 后發(fā)現(xiàn) uniqueInstance 不為空,因此返回 uniqueInstance,但此時 uniqueInstance 還未被初始化。
使用 volatile 可以禁止 JVM 的指令重排,保證在多線程環(huán)境下也能正常運行。
底層原理
synchronized 同步語句塊的情況
public class SynchronizedDemo {public void method() {synchronized (this) {System.out.println("synchronized 代碼塊");}}}
通過 JDK 自帶的 javap 命令查看 SynchronizedDemo 類的相關(guān)字節(jié)碼信息:首先切換到類的對應(yīng)目錄執(zhí)行javac SynchronizedDemo.java命令生成編譯后的 .class 文件,然后執(zhí)行javap -c -s -v -l SynchronizedDemo.class

從上面我們可以看出:
synchronized 同步語句塊的實現(xiàn)使用的是 monitorenter和 monitorexit指令,其中 monitorenter 指令指向同步代碼塊的開始位置,monitorexit 指令則指明同步代碼塊的結(jié)束位置。
當(dāng)執(zhí)行 monitorenter 指令時,線程試圖獲取鎖也就是獲取 monitor(monitor對象存在于每個Java對象的對象頭中,synchronized 鎖便是通過這種方式獲取鎖的,也是為什么Java中任意對象可以作為鎖的原因) 的持有權(quán)。當(dāng)計數(shù)器為0則可以成功獲取,獲取后將鎖計數(shù)器設(shè)為1也就是加1。相應(yīng)的在執(zhí)行 monitorexit 指令后,將鎖計數(shù)器設(shè)為0,表明鎖被釋放。如果獲取對象鎖失敗,那當(dāng)前線程就要阻塞等待,直到鎖被另外一個線程釋放為止。
synchronized 修飾方法的的情況
public class SynchronizedDemo2 {public synchronized void method() {System.out.println("synchronized 方法");}}

synchronized 修飾的方法并沒有 monitorenter 指令和 monitorexit 指令,取得代之的確實是 ACC_SYNCHRONIZED標(biāo)識,該標(biāo)識指明了該方法是一個同步方法,JVM 通過該訪問標(biāo)志來
辨別一個方法是否聲明為同步方法,從而執(zhí)行相應(yīng)的同步調(diào)用。
synchronized和ReenTrantLock 的區(qū)別
兩者都是可重入鎖
兩者都是可重入鎖。“可重入鎖”概念是:自己可以再次獲取自己的內(nèi)部鎖。比如一個線程獲得了某個對象的鎖,此時這個對象鎖還沒有釋放,當(dāng)其再次想要獲取這個對象的鎖的時候還是可以獲取的,如果不可鎖重入的話,就會造成死鎖。同一個線程每次獲取鎖,鎖的計數(shù)器都自增1,所以要等到鎖的計數(shù)器下降為0時才能釋放鎖。synchronized 依賴于 JVM 而 ReenTrantLock 依賴于 API
synchronized 是依賴于 JVM 實現(xiàn)的,前面我們也講到了 虛擬機(jī)團(tuán)隊在 JDK1.6 為 synchronized 關(guān)鍵字進(jìn)行了很多優(yōu)化,但是這些優(yōu)化都是在虛擬機(jī)層面實現(xiàn)的,并沒有直接暴露給我們。ReenTrantLock 是 JDK 層面實現(xiàn)的(也就是 API 層面,需要 lock() 和 unlock 方法配合 try/finally 語句塊來完成),所以我們可以通過查看它的源代碼,來看它是如何實現(xiàn)的。ReenTrantLock 比 synchronized 增加了一些高級功能
相比synchronized,ReenTrantLock增加了一些高級功能。主要來說主要有三點:等待可中斷;
可實現(xiàn)公平鎖;
可實現(xiàn)選擇性通知(鎖可以綁定多個條件)
ReenTrantLock提供了一種能夠中斷等待鎖的線程的機(jī)制,通過lock.lockInterruptibly()來實現(xiàn)這個機(jī)制。也就是說正在等待的線程可以選擇放棄等待,改為處理其他事情。
ReenTrantLock可以指定是公平鎖還是非公平鎖。而synchronized只能是非公平鎖。所謂的公平鎖就是先等待的線程先獲得鎖。ReenTrantLock默認(rèn)情況是非公平的,可以通過 ReenTrantLock類的ReentrantLock(boolean fair) 構(gòu)造方法來制定是否是公平的。
synchronized關(guān)鍵字與wait()和notify/notifyAll()方法相結(jié)合可以實現(xiàn)等待/通知機(jī)制,ReentrantLock類當(dāng)然也可以實現(xiàn),但是需要借助于Condition接口與newCondition() 方法。Condition是JDK1.5之后才有的,它具有很好的靈活性,比如可以實現(xiàn)多路通知功能也就是在一個Lock對象中可以創(chuàng)建多個Condition實例(即對象監(jiān)視器),線程對象可以注冊在指定的Condition中,從而可以有選擇性地進(jìn)行線程通知,在調(diào)度線程上更加靈活。在使用notify/notifyAll()方法進(jìn)行通知時,被通知的線程是由 JVM 選擇的,用ReentrantLock類結(jié)合Condition實例可以實現(xiàn)“選擇性通知” ,這個功能非常重要,而且是Condition接口默認(rèn)提供的。而synchronized關(guān)鍵字就相當(dāng)于整個Lock對象中只有一個Condition實例,所有的線程都注冊在它一個身上。如果執(zhí)行notifyAll()方法的話就會通知所有處于等待狀態(tài)的線程這樣會造成很大的效率問題,而Condition實例的signalAll()方法 只會喚醒注冊在該Condition實例中的所有等待線程。

鎖的不是代碼。是對象



對象在內(nèi)存中的布局:對象頭、實例數(shù)據(jù)、對齊填充


釋放的時機(jī)
總結(jié)下使用synchronized同步鎖釋放的時機(jī)。我們知道程序執(zhí)行進(jìn)入同步代碼塊中monitorenter代表嘗試獲取鎖,退出代碼塊monitorexit代表釋放鎖。而在程序中,是無法顯式釋放對同步監(jiān)視器的鎖的,而會在如下4種情況下釋放鎖。
當(dāng)前線程的同步方法、代碼塊執(zhí)行結(jié)束的時候釋放
當(dāng)前線程在同步方法、同步代碼塊中遇到break 、 return 終于該代碼塊或者方法的時候釋放。
出現(xiàn)未處理的error或者exception導(dǎo)致異常結(jié)束的時候釋放
程序執(zhí)行了 同步對象 wait 方法 ,當(dāng)前線程暫停,釋放鎖
在以下兩種情況不會釋放鎖。
代碼塊中使用了 Thread.sleep() Thread.yield() 這些方法暫停線程的執(zhí)行,不會釋放。
線程執(zhí)行同步代碼塊時,其他線程調(diào)用 suspend 方法將該線程掛起,該線程不會釋放鎖 ,所以我們應(yīng)該避免使用 suspend 和 resume 來控制線程 。
JVM 對 synchronized 的鎖優(yōu)化
自旋鎖
鎖消除
鎖消除是指對于被檢測出不可能存在競爭的共享數(shù)據(jù)的鎖進(jìn)行消除。
鎖消除主要是通過逃逸分析來支持,如果堆上的共享數(shù)據(jù)不可能逃逸出去被其它線程訪問到,那么就可以把它們當(dāng)成私有數(shù)據(jù)對待,也就可以將它們的鎖進(jìn)行消除。
對于一些看起來沒有加鎖的代碼,其實隱式的加了很多鎖。例如下面的字符串拼接代碼就隱式加了鎖:
public static String concatString(String s1, String s2, String s3) {return s1 + s2 + s3;}
String 是一個不可變的類,編譯器會對 String 的拼接自動優(yōu)化。在 JDK 1.5 之前,會轉(zhuǎn)化為 StringBuffer 對象的連續(xù) append() 操作:
public static String concatString(String s1, String s2, String s3) {StringBuffer sb = new StringBuffer();sb.append(s1);sb.append(s2);sb.append(s3);return sb.toString();}
每個 append() 方法中都有一個同步塊。虛擬機(jī)觀察變量 sb,很快就會發(fā)現(xiàn)它的動態(tài)作用域被限制在 concatString() 方法內(nèi)部。也就是說,sb 的所有引用永遠(yuǎn)不會逃逸到 concatString() 方法之外,其他線程無法訪問到它,因此可以進(jìn)行消除。
鎖粗化
如果一系列的連續(xù)操作都對同一個對象反復(fù)加鎖和解鎖,頻繁的加鎖操作就會導(dǎo)致性能損耗。
上一節(jié)的示例代碼中連續(xù)的 append() 方法就屬于這類情況。如果虛擬機(jī)探測到由這樣的一串零碎的操作都對同一個對象加鎖,將會把加鎖的范圍擴(kuò)展(粗化)到整個操作序列的外部。對于上一節(jié)的示例代碼就是擴(kuò)展到第一個 append() 操作之前直至最后一個 append() 操作之后,這樣只需要加鎖一次就可以了。
輕量級鎖
JDK 1.6 引入了偏向鎖和輕量級鎖,從而讓鎖擁有了四個狀態(tài):無鎖狀態(tài)(unlocked)、偏向鎖狀態(tài)(biasble)、輕量級鎖狀態(tài)(lightweight locked)和重量級鎖狀態(tài)(inflated)。
下圖左側(cè)是一個線程的虛擬機(jī)棧,其中有一部分稱為 Lock Record 的區(qū)域,這是在輕量級鎖運行過程創(chuàng)建的,用于存放鎖對象的 Mark Word。而右側(cè)就是一個鎖對象,包含了 Mark Word 和其它信息。

輕量級鎖是相對于傳統(tǒng)的重量級鎖而言,它使用 CAS 操作來避免重量級鎖使用互斥量的開銷。對于絕大部分的鎖,在整個同步周期內(nèi)都是不存在競爭的,因此也就不需要都使用互斥量進(jìn)行同步,可以先采用 CAS 操作進(jìn)行同步,如果 CAS 失敗了再改用互斥量進(jìn)行同步。
加鎖過程:
在代碼進(jìn)入同步塊的時候,如果同步對象鎖狀態(tài)為無鎖狀態(tài)(鎖標(biāo)志位為“01”狀態(tài),是否為偏向鎖為“0”),虛擬機(jī)首先將在當(dāng)前線程的棧幀中建立一個名為鎖記錄(Lock Record)的空間,用于存儲鎖對象目前的Mark Word的拷貝,官方稱之為 Displaced Mark Word。這時候線程堆棧與對象頭的狀態(tài)如圖1所示。
拷貝對象頭中的Mark Word復(fù)制到鎖記錄中。
拷貝成功后,虛擬機(jī)將使用CAS操作嘗試將對象的Mark Word更新為指向Lock Record的指針,并將Lock record里的owner指針指向object mark word。如果更新成功,則執(zhí)行步驟(3),否則執(zhí)行步驟(4)。
如果這個更新動作成功了,那么這個線程就擁有了該對象的鎖,并且對象Mark Word的鎖標(biāo)志位設(shè)置為“00”,即表示此對象處于輕量級鎖定狀態(tài),這時候線程堆棧與對象頭的狀態(tài)如圖2所示。
如果這個更新操作失敗了,虛擬機(jī)首先會檢查對象的Mark Word是否指向當(dāng)前線程的棧幀,如果是就說明當(dāng)前線程已經(jīng)擁有了這個對象的鎖,那就可以直接進(jìn)入同步塊繼續(xù)執(zhí)行。否則說明多個線程競爭鎖,輕量級鎖就要膨脹為重量級鎖,鎖標(biāo)志的狀態(tài)值變?yōu)椤?0”,Mark Word中存儲的就是指向重量級鎖(互斥量)的指針,后面等待鎖的線程也要進(jìn)入阻塞狀態(tài)。而當(dāng)前線程便嘗試使用自旋來獲取鎖,自旋就是為了不讓線程阻塞,而采用循環(huán)去獲取鎖的過程。

如果 CAS 操作失敗了,虛擬機(jī)首先會檢查對象的 Mark Word 是否指向當(dāng)前線程的虛擬機(jī)棧,如果是的話說明當(dāng)前線程已經(jīng)擁有了這個鎖對象,那就可以直接進(jìn)入同步塊繼續(xù)執(zhí)行,否則說明這個鎖對象已經(jīng)被其他線程線程搶占了。如果有兩條以上的線程爭用同一個鎖,那輕量級鎖就不再有效,要膨脹為重量級鎖。
解鎖過程:
通過CAS操作嘗試把線程中復(fù)制的Displaced Mark Word對象替換當(dāng)前的Mark Word。
如果替換成功,整個同步過程就完成了。
如果替換失敗,說明有其他線程嘗試過獲取該鎖(此時鎖已膨脹),那就要在釋放鎖的同時,喚醒被掛起的線程。
偏向鎖
引入偏向鎖是為了在無多線程競爭的情況下盡量減少不必要的輕量級鎖執(zhí)行路徑,因為輕量級鎖的獲取及釋放依賴多次CAS原子指令,而偏向鎖只需要在置換ThreadID的時候依賴一次CAS原子指令(由于一旦出現(xiàn)多線程競爭的情況就必須撤銷偏向鎖,所以偏向鎖的撤銷操作的性能損耗必須小于節(jié)省下來的CAS原子指令的性能消耗)。上面說過,輕量級鎖是為了在線程交替執(zhí)行同步塊時提高性能,而偏向鎖則是在只有一個線程執(zhí)行同步塊時進(jìn)一步提高性能。
偏向鎖獲取過程:
訪問Mark Word中偏向鎖的標(biāo)識是否設(shè)置成1,鎖標(biāo)志位是否為01——確認(rèn)為可偏向狀態(tài)。
如果為可偏向狀態(tài),則測試線程ID是否指向當(dāng)前線程,如果是,進(jìn)入步驟(5),否則進(jìn)入步驟(3)。
如果線程ID并未指向當(dāng)前線程,則通過CAS操作競爭鎖。如果競爭成功,則將Mark Word中線程ID設(shè)置為當(dāng)前線程ID,然后執(zhí)行(5);如果競爭失敗,執(zhí)行(4)。
如果CAS獲取偏向鎖失敗,則表示有競爭。當(dāng)?shù)竭_(dá)全局安全點(safepoint)時獲得偏向鎖的線程被掛起,偏向鎖升級為輕量級鎖,然后被阻塞在安全點的線程繼續(xù)往下執(zhí)行同步代碼。
執(zhí)行同步代碼。
偏向鎖的釋放:
偏向鎖的撤銷在上述第四步驟中有提到:偏向鎖只有遇到其他線程嘗試競爭偏向鎖時,持有偏向鎖的線程才會釋放鎖,線程不會主動去釋放偏向鎖。
偏向鎖的撤銷,需要等待全局安全點(在這個時間點上沒有字節(jié)碼正在執(zhí)行),它會首先暫停擁有偏向鎖的線程,判斷鎖對象是否處于被鎖定狀態(tài),撤銷偏向鎖后恢復(fù)到未鎖定(標(biāo)志位為“01”)或輕量級鎖(標(biāo)志位為“00”)的狀態(tài)。
重量級鎖、輕量級鎖和偏向鎖之間轉(zhuǎn)換

記得點「贊」和「在看」↓
愛你們
