Lock和synchronized該如何選擇?
synchronized和lock比較淺析
synchronized是基于jvm底層實(shí)現(xiàn)的數(shù)據(jù)同步,lock是基于Java編寫(xiě),主要通過(guò)硬件依賴(lài)CPU指令實(shí)現(xiàn)數(shù)據(jù)同步。下面一一介紹
一、synchronized的實(shí)現(xiàn)方案
1.synchronized能夠把任何一個(gè)非null對(duì)象當(dāng)成鎖,實(shí)現(xiàn)由兩種方式:
a.當(dāng)synchronized作用于非靜態(tài)方法時(shí),鎖住的是當(dāng)前對(duì)象的事例,當(dāng)synchronized作用于靜態(tài)方法時(shí),鎖住的是class實(shí)例,又因?yàn)镃lass的相關(guān)數(shù)據(jù)存儲(chǔ)在永久帶,因此靜態(tài)方法鎖相當(dāng)于類(lèi)的一個(gè)全局鎖。
b.當(dāng)synchronized作用于一個(gè)對(duì)象實(shí)例時(shí),鎖住的是對(duì)應(yīng)的代碼塊。
2.synchronized鎖又稱(chēng)為對(duì)象監(jiān)視器(object)。 3.當(dāng)多個(gè)線(xiàn)程一起訪(fǎng)問(wèn)某個(gè)對(duì)象監(jiān)視器的時(shí)候,對(duì)象監(jiān)視器會(huì)將這些請(qǐng)求存儲(chǔ)在不同的容器中。
>Contention List:競(jìng)爭(zhēng)隊(duì)列,所有請(qǐng)求鎖的線(xiàn)程首先被放在這個(gè)競(jìng)爭(zhēng)隊(duì)列中
>Entry List:Contention List中那些有資格成為候選資源的線(xiàn)程被移動(dòng)到Entry List中
>Wait Set:哪些調(diào)用wait方法被阻塞的線(xiàn)程被放置在這里
>OnDeck:任意時(shí)刻,最多只有一個(gè)線(xiàn)程正在競(jìng)爭(zhēng)鎖資源,該線(xiàn)程被成為OnDeck
>Owner:當(dāng)前已經(jīng)獲取到所資源的線(xiàn)程被稱(chēng)為Owner
> !Owner:當(dāng)前釋放鎖的線(xiàn)程
下圖展示了他們之前的關(guān)系
4.synchronized在jdk1.6之后提供了多種優(yōu)化方案:
>自旋鎖
jdk1.6之后默認(rèn)開(kāi)啟,可以使用參數(shù)-XX:+UseSpinning控制,自旋等待不能代替阻塞,且先不說(shuō)對(duì)處理器數(shù)量的要求,自旋等待本身雖然避免了線(xiàn)程切換的開(kāi)銷(xiāo),但它是要占用處理器時(shí)間的,因此,如果鎖被占用的時(shí)間很短,自旋等待的效果就會(huì)非常好,反之,如果鎖被占用的時(shí)候很長(zhǎng),那么自旋的線(xiàn)程只會(huì)白白消耗處理器資源,而不會(huì)做任何有用的工作,反而會(huì)帶來(lái)性能上的浪費(fèi)。自旋次數(shù)的默認(rèn)值是 10 次,用戶(hù)可以使用參數(shù) -XX:PreBlockSpin 來(lái)更改。
自旋鎖的本質(zhì):執(zhí)行幾個(gè)空方法,稍微等一等,也許是一段時(shí)間的循環(huán),也許是幾行空的匯編指令。
>鎖消除
即時(shí)編譯器在運(yùn)行時(shí),對(duì)一些代碼上要求同步,但是被檢測(cè)到不可能存在共享數(shù)據(jù)競(jìng)爭(zhēng)的鎖進(jìn)行消除,依據(jù)來(lái)源于逃逸分析的數(shù)據(jù)支持,那么是什么是逃逸分析?對(duì)于虛擬機(jī)來(lái)說(shuō)需要使用數(shù)據(jù)流分析來(lái)確定是否消除變量底層框架的同步代碼,因?yàn)橛性S多同步的代碼不是自己寫(xiě)的。
例1.1
public static String concatString(String s1, String s2, String s3) { return s1 + s2 + s3; }
由于 String 是一個(gè)不可變的類(lèi),對(duì)字符串的連接操作總是通過(guò)生成新的 String 對(duì)象來(lái)進(jìn)行的,因此 Javac 編譯器會(huì)對(duì) String 連接做自動(dòng)優(yōu)化。在 JDK 1.5 之前,會(huì)轉(zhuǎn)化為 StringBuffer 對(duì)象的連續(xù) append() 操作,在 JDK 1.5 及以后的版本中,會(huì)轉(zhuǎn)化為 StringBuilder 對(duì)象的連續(xù) append() 操作,這里的stringBuilder.append是線(xiàn)程不同步的(假設(shè)是同步)。
Javac 轉(zhuǎn)化后的字符串連接代碼為:
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(); }
此時(shí)的鎖對(duì)象就是sb,虛擬機(jī)觀(guān)察變量 sb,很快就會(huì)發(fā)現(xiàn)它的動(dòng)態(tài)作用域被限制在 concatString() 方法內(nèi)部。也就是說(shuō),sb 的所有引用永遠(yuǎn)不會(huì) “逃逸” 到concatString() 方法之外,其他線(xiàn)程無(wú)法訪(fǎng)問(wèn)到它,雖然這里有鎖,但是可以被安全地消除掉,在即時(shí)編譯之后,這段代碼就會(huì)忽略掉所有的同步而直接執(zhí)行了。
>鎖粗化
將同步塊的作用范圍限制得盡量小——只在共享數(shù)據(jù)的實(shí)際作用域中才進(jìn)行同步,這樣是為了使得需要同步的操作數(shù)量盡可能變小,如果存在鎖競(jìng)爭(zhēng),那等待鎖的線(xiàn)程也能盡快拿到鎖。
>輕量級(jí)鎖
加鎖過(guò)程:在代碼進(jìn)入同步塊的時(shí)候,如果此同步對(duì)象沒(méi)有被鎖定(鎖標(biāo)志位為 “01” 狀態(tài))虛擬機(jī)首先將在當(dāng)前線(xiàn)程的棧幀中建立一個(gè)名為鎖記錄(Lock Record)的空間,用于存儲(chǔ)鎖對(duì)象目前的 Mark Word 的拷貝,這時(shí)候線(xiàn)程堆棧與對(duì)象頭的狀態(tài)如圖 13-3 所示
然后,虛擬機(jī)將使用 CAS 操作嘗試將對(duì)象的 Mark Word 更新為指向 Lock Record 的指針。如果這個(gè)更新動(dòng)作成功了,那么這個(gè)線(xiàn)程就擁有了該對(duì)象的鎖,并且對(duì)象 Mark Word 的鎖標(biāo)志位 (Mark Word 的最后 2bit)將轉(zhuǎn)變?yōu)?“00”,即表示此對(duì)象處于輕量級(jí)鎖定狀態(tài),這時(shí)線(xiàn)程堆棧與對(duì)象頭的狀態(tài)如圖13-4
如果上述更新操作失敗,則說(shuō)明這個(gè)鎖對(duì)象被其他鎖占用,此時(shí)輕量級(jí)變?yōu)橹亓考?jí)鎖,標(biāo)志位為“10”,后面等待的線(xiàn)程進(jìn)入阻塞狀態(tài)。
解鎖過(guò)程:也是由CAS進(jìn)行操作的,如果對(duì)象的 Mark Word 仍然指向著線(xiàn)程的鎖記錄,那就用 CAS 操作把對(duì)象當(dāng)前的 Mark Word 和線(xiàn)程中復(fù)制的 Displaced Mark Word 替換回來(lái),如果替換成功,整個(gè)同步過(guò)程就完成了。如果替換失敗,說(shuō)明有其他線(xiàn)程嘗試過(guò)獲取該鎖,那就要釋放鎖的同時(shí),喚醒被掛起的線(xiàn)程。
輕量級(jí)鎖能提升程序同步性能的依據(jù)是 “對(duì)于絕大部分的鎖,在整個(gè)同步周期內(nèi)都是不存在競(jìng)爭(zhēng)的”,這是一個(gè)經(jīng)驗(yàn)數(shù)據(jù)。如果沒(méi)有競(jìng)爭(zhēng),輕量級(jí)鎖使用 CAS 操作避免了使用互斥量的開(kāi)銷(xiāo),但如果存在鎖競(jìng)爭(zhēng),除了互斥量的開(kāi)銷(xiāo)外,還額外發(fā)生了 CAS 操作,因此在有競(jìng)爭(zhēng)的情況下,輕量級(jí)鎖會(huì)比傳統(tǒng)的重量級(jí)鎖更慢。
>偏向鎖
偏向鎖也是 JDK 1.6 中引入的一項(xiàng)鎖優(yōu)化,它的目的是消除數(shù)據(jù)在無(wú)競(jìng)爭(zhēng)情況下的同步原語(yǔ),進(jìn)一步提高程序的運(yùn)行性能。如果說(shuō)輕量級(jí)鎖是在無(wú)競(jìng)爭(zhēng)的情況下使用 CAS 操作去消除同步使用的互斥量,那偏向鎖就是在無(wú)競(jìng)爭(zhēng)的情況下把整個(gè)同步都消除掉,連 CAS 操作都不做了。
實(shí)質(zhì)就是設(shè)置一個(gè)變量,判斷這個(gè)變量是否是當(dāng)前線(xiàn)程,是就避免再次加鎖解鎖操作,從而避免了多次的CAS操作。壞處是如果一個(gè)線(xiàn)程持有偏向鎖,另外一個(gè)線(xiàn)程想爭(zhēng)用偏向?qū)ο螅瑩碛姓呦脶尫胚@個(gè)偏向鎖,釋放會(huì)帶來(lái)額外的性能開(kāi)銷(xiāo),但是總體來(lái)說(shuō)偏向鎖帶來(lái)的好處還是大于CAS的代價(jià)的。在具體問(wèn)題具體分析的前提下,有時(shí)候使用參數(shù) -XX:-UseBiasedLocking 來(lái)禁止偏向鎖優(yōu)化反而可以提升性能。
二、lock的實(shí)現(xiàn)方案
與synchronized不同的是lock是純java手寫(xiě)的,與底層的JVM無(wú)關(guān)。在java.util.concurrent.locks包中有很多Lock的實(shí)現(xiàn)類(lèi),常用的有ReenTrantLock、ReadWriteLock(實(shí)現(xiàn)類(lèi)有ReenTrantReadWriteLock)
,其實(shí)現(xiàn)都依賴(lài)java.util.concurrent.AbstractQueuedSynchronizer類(lèi)(簡(jiǎn)稱(chēng)AQS),實(shí)現(xiàn)思路都大同小異,因此我們以ReentrantLock作為講解切入點(diǎn)。
分析之前我們先來(lái)花點(diǎn)時(shí)間看下AQS。AQS是我們后面將要提到的CountDownLatch/FutureTask/ReentrantLock/RenntrantReadWriteLock/Semaphore的基礎(chǔ),因此AQS也是Lock和Excutor實(shí)現(xiàn)的基礎(chǔ)。它的基本思想就是一個(gè)同步器,支持獲取鎖和釋放鎖兩個(gè)操作。
要支持上面鎖獲取、釋放鎖就必須滿(mǎn)足下面的條件:
1、 狀態(tài)位必須是原子操作的
2、 阻塞和喚醒線(xiàn)程
3、 一個(gè)有序的隊(duì)列,用于支持鎖的公平性
場(chǎng)景:可定時(shí)的、可輪詢(xún)的與可中斷的鎖獲取操作,公平隊(duì)列,或者非塊結(jié)構(gòu)的鎖。
主要從以下幾個(gè)特點(diǎn)介紹:
1.可重入鎖
如果鎖具備可重入性,則稱(chēng)作為可重入鎖。像synchronized和ReentrantLock都是可重入鎖,可重入性在我看來(lái)實(shí)際上表明了鎖的分配機(jī)制:基于線(xiàn)程的分配,而不是基于方法調(diào)用的分配。
2.可中斷鎖
可中斷鎖:顧名思義,就是可以相應(yīng)中斷的鎖。
在Java中,synchronized就不是可中斷鎖,而Lock是可中斷鎖。
如果某一線(xiàn)程A正在執(zhí)行鎖中的代碼,另一線(xiàn)程B正在等待獲取該鎖,可能由于等待時(shí)間過(guò)長(zhǎng),線(xiàn)程B不想等待了,想先處理其他事情,我們可以讓它中斷自己或者在別的線(xiàn)程中中斷它,這種就是可中斷鎖。
3.公平鎖和非公平鎖
公平鎖以請(qǐng)求鎖的順序來(lái)獲取鎖,非公平鎖則是無(wú)法保證按照請(qǐng)求的順序執(zhí)行。synchronized就是非公平鎖,它無(wú)法保證等待的線(xiàn)程獲取鎖的順序。而對(duì)于ReentrantLock和ReentrantReadWriteLock,它默認(rèn)情況下是非公平鎖,但是可以設(shè)置為公平鎖。
參數(shù)為true時(shí)表示公平鎖,不傳或者false都是為非公平鎖。
ReentrantLock lock = new ReentrantLock(true);
4.讀寫(xiě)鎖
讀寫(xiě)鎖將對(duì)一個(gè)資源(比如文件)的訪(fǎng)問(wèn)分成了2個(gè)鎖,一個(gè)讀鎖和一個(gè)寫(xiě)鎖。
正因?yàn)橛辛俗x寫(xiě)鎖,才使得多個(gè)線(xiàn)程之間的讀操作不會(huì)發(fā)生沖突。
ReadWriteLock就是讀寫(xiě)鎖,它是一個(gè)接口,ReentrantReadWriteLock實(shí)現(xiàn)了這個(gè)接口。
可以通過(guò)readLock()獲取讀鎖,通過(guò)writeLock()獲取寫(xiě)鎖。
三、總結(jié)
1.synchronized
優(yōu)點(diǎn):實(shí)現(xiàn)簡(jiǎn)單,語(yǔ)義清晰,便于JVM堆棧跟蹤,加鎖解鎖過(guò)程由JVM自動(dòng)控制,提供了多種優(yōu)化方案,使用更廣泛
缺點(diǎn):悲觀(guān)的排他鎖,不能進(jìn)行高級(jí)功能
2.lock
優(yōu)點(diǎn):可定時(shí)的、可輪詢(xún)的與可中斷的鎖獲取操作,提供了讀寫(xiě)鎖、公平鎖和非公平鎖
缺點(diǎn):需手動(dòng)釋放鎖unlock,不適合JVM進(jìn)行堆棧跟蹤
3.相同點(diǎn)
都是可重入鎖