阿里面試失敗后,一氣之下我圖解了Java中18把鎖

  • 樂(lè)觀鎖和悲觀鎖
  • 獨(dú)占鎖和共享鎖
  • 互斥鎖和讀寫(xiě)鎖
  • 公平鎖和非公平鎖
  • 可重入鎖
  • 自旋鎖
  • 分段鎖
  • 鎖升級(jí)(無(wú)鎖|偏向鎖|輕量級(jí)鎖|重量級(jí)鎖)
  • 鎖優(yōu)化技術(shù)(鎖粗化议谷、鎖消除)

樂(lè)觀鎖和悲觀鎖

悲觀鎖

悲觀鎖對(duì)應(yīng)于生活中悲觀的人炉爆,悲觀的人總是想著事情往壞的方向發(fā)展。

舉個(gè)生活中的例子卧晓,假設(shè)廁所只有一個(gè)坑位了芬首,悲觀鎖上廁所會(huì)第一時(shí)間把門(mén)反鎖上,這樣其他人上廁所只能在門(mén)外等候禀崖,這種狀態(tài)就是「阻塞」了衩辟。

回到代碼世界中,一個(gè)共享數(shù)據(jù)加了悲觀鎖波附,那線程每次想操作這個(gè)數(shù)據(jù)前都會(huì)假設(shè)其他線程也可能會(huì)操作這個(gè)數(shù)據(jù),所以每次操作前都會(huì)上鎖昼钻,這樣其他線程想操作這個(gè)數(shù)據(jù)拿不到鎖只能阻塞了掸屡。


在 Java 語(yǔ)言中 synchronized 和 ReentrantLock等就是典型的悲觀鎖,還有一些使用了 synchronized 關(guān)鍵字的容器類(lèi)如 HashTable 等也是悲觀鎖的應(yīng)用然评。

樂(lè)觀鎖

樂(lè)觀鎖 對(duì)應(yīng)于生活中樂(lè)觀的人仅财,樂(lè)觀的人總是想著事情往好的方向發(fā)展。

舉個(gè)生活中的例子碗淌,假設(shè)廁所只有一個(gè)坑位了盏求,樂(lè)觀鎖認(rèn)為:這荒郊野外的,又沒(méi)有什么人亿眠,不會(huì)有人搶我坑位的碎罚,每次關(guān)門(mén)上鎖多浪費(fèi)時(shí)間,還是不加鎖好了纳像。你看樂(lè)觀鎖就是天生樂(lè)觀荆烈!

回到代碼世界中,樂(lè)觀鎖操作數(shù)據(jù)時(shí)不會(huì)上鎖,在更新的時(shí)候會(huì)判斷一下在此期間是否有其他線程去更新這個(gè)數(shù)據(jù)憔购。


樂(lè)觀鎖可以使用版本號(hào)機(jī)制和CAS算法實(shí)現(xiàn)宫峦。在 Java 語(yǔ)言中 java.util.concurrent.atomic包下的原子類(lèi)就是使用CAS 樂(lè)觀鎖實(shí)現(xiàn)的。

兩種鎖的使用場(chǎng)景

悲觀鎖和樂(lè)觀鎖沒(méi)有孰優(yōu)孰劣玫鸟,有其各自適應(yīng)的場(chǎng)景导绷。

樂(lè)觀鎖適用于寫(xiě)比較少(沖突比較小)的場(chǎng)景屎飘,因?yàn)椴挥蒙湘i妥曲、釋放鎖,省去了鎖的開(kāi)銷(xiāo)枚碗,從而提升了吞吐量逾一。

如果是寫(xiě)多讀少的場(chǎng)景,即沖突比較嚴(yán)重肮雨,線程間競(jìng)爭(zhēng)激勵(lì)遵堵,使用樂(lè)觀鎖就是導(dǎo)致線程不斷進(jìn)行重試,這樣可能還降低了性能怨规,這種場(chǎng)景下使用悲觀鎖就比較合適陌宿。

獨(dú)占鎖和共享鎖

獨(dú)占鎖

獨(dú)占鎖是指鎖一次只能被一個(gè)線程所持有。如果一個(gè)線程對(duì)數(shù)據(jù)加上排他鎖后波丰,那么其他線程不能再對(duì)該數(shù)據(jù)加任何類(lèi)型的鎖壳坪。獲得獨(dú)占鎖的線程即能讀數(shù)據(jù)又能修改數(shù)據(jù)。


JDK中的synchronized和java.util.concurrent(JUC)包中Lock的實(shí)現(xiàn)類(lèi)就是獨(dú)占鎖掰烟。

共享鎖

共享鎖是指鎖可被多個(gè)線程所持有爽蝴。如果一個(gè)線程對(duì)數(shù)據(jù)加上共享鎖后,那么其他線程只能對(duì)數(shù)據(jù)再加共享鎖纫骑,不能加獨(dú)占鎖蝎亚。獲得共享鎖的線程只能讀數(shù)據(jù),不能修改數(shù)據(jù)先馆。


在 JDK 中 ReentrantReadWriteLock 就是一種共享鎖发框。

互斥鎖和讀寫(xiě)鎖

互斥鎖

互斥鎖是獨(dú)占鎖的一種常規(guī)實(shí)現(xiàn),是指某一資源同時(shí)只允許一個(gè)訪問(wèn)者對(duì)其進(jìn)行訪問(wèn)煤墙,具有唯一性和排它性梅惯。


互斥鎖一次只能一個(gè)線程擁有互斥鎖,其他線程只有等待仿野。

讀寫(xiě)鎖

讀寫(xiě)鎖是共享鎖的一種具體實(shí)現(xiàn)铣减。讀寫(xiě)鎖管理一組鎖,一個(gè)是只讀的鎖设预,一個(gè)是寫(xiě)鎖徙歼。

讀鎖可以在沒(méi)有寫(xiě)鎖的時(shí)候被多個(gè)線程同時(shí)持有,而寫(xiě)鎖是獨(dú)占的。寫(xiě)鎖的優(yōu)先級(jí)要高于讀鎖魄梯,一個(gè)獲得了讀鎖的線程必須能看到前一個(gè)釋放的寫(xiě)鎖所更新的內(nèi)容桨螺。

讀寫(xiě)鎖相比于互斥鎖并發(fā)程度更高,每次只有一個(gè)寫(xiě)線程酿秸,但是同時(shí)可以有多個(gè)線程并發(fā)讀灭翔。


在 JDK 中定義了一個(gè)讀寫(xiě)鎖的接口:ReadWriteLock

public interface ReadWriteLock {
    /**
     * 獲取讀鎖
     */
    Lock readLock();

    /**
     * 獲取寫(xiě)鎖
     */
    Lock writeLock();
}

ReentrantReadWriteLock 實(shí)現(xiàn)了ReadWriteLock接口,具體實(shí)現(xiàn)這里不展開(kāi)辣苏,后續(xù)會(huì)深入源碼解析肝箱。

公平鎖和非公平鎖

公平鎖

公平鎖是指多個(gè)線程按照申請(qǐng)鎖的順序來(lái)獲取鎖,這里類(lèi)似排隊(duì)買(mǎi)票稀蟋,先來(lái)的人先買(mǎi)煌张,后來(lái)的人在隊(duì)尾排著,這是公平的退客。


在 java 中可以通過(guò)構(gòu)造函數(shù)初始化公平鎖

/**
* 創(chuàng)建一個(gè)可重入鎖骏融,true 表示公平鎖,false 表示非公平鎖萌狂。默認(rèn)非公平鎖
*/
Lock lock = new ReentrantLock(true);

非公平鎖

非公平鎖是指多個(gè)線程獲取鎖的順序并不是按照申請(qǐng)鎖的順序档玻,有可能后申請(qǐng)的線程比先申請(qǐng)的線程優(yōu)先獲取鎖,在高并發(fā)環(huán)境下茫藏,有可能造成優(yōu)先級(jí)翻轉(zhuǎn)误趴,或者饑餓的狀態(tài)(某個(gè)線程一直得不到鎖)。

在 java 中 synchronized 關(guān)鍵字是非公平鎖务傲,ReentrantLock默認(rèn)也是非公平鎖凉当。

/**
* 創(chuàng)建一個(gè)可重入鎖,true 表示公平鎖售葡,false 表示非公平鎖纤怒。默認(rèn)非公平鎖
*/
Lock lock = new ReentrantLock(false);

可重入鎖

可重入鎖又稱(chēng)之為遞歸鎖,是指同一個(gè)線程在外層方法獲取了鎖天通,在進(jìn)入內(nèi)層方法會(huì)自動(dòng)獲取鎖。


對(duì)于Java ReentrantLock而言, 他的名字就可以看出是一個(gè)可重入鎖熄驼。對(duì)于Synchronized而言像寒,也是一個(gè)可重入鎖。

敲黑板:可重入鎖的一個(gè)好處是可一定程度避免死鎖瓜贾。

以 synchronized 為例诺祸,看一下下面的代碼:

public synchronized void mehtodA() throws Exception{
 // Do some magic tings
 mehtodB();
}

public synchronized void mehtodB() throws Exception{
 // Do some magic tings
}

上面的代碼中 methodA 調(diào)用 methodB,如果一個(gè)線程調(diào)用methodA 已經(jīng)獲取了鎖再去調(diào)用 methodB 就不需要再次獲取鎖了祭芦,這就是可重入鎖的特性筷笨。如果不是可重入鎖的話,mehtodB 可能不會(huì)被當(dāng)前線程執(zhí)行,可能造成死鎖胃夏。

自旋鎖

自旋鎖是指線程在沒(méi)有獲得鎖時(shí)不是被直接掛起轴或,而是執(zhí)行一個(gè)忙循環(huán),這個(gè)忙循環(huán)就是所謂的自旋仰禀。


自旋鎖的目的是為了減少線程被掛起的幾率照雁,因?yàn)榫€程的掛起和喚醒也都是耗資源的操作。

如果鎖被另一個(gè)線程占用的時(shí)間比較長(zhǎng)答恶,即使自旋了之后當(dāng)前線程還是會(huì)被掛起饺蚊,忙循環(huán)就會(huì)變成浪費(fèi)系統(tǒng)資源的操作,反而降低了整體性能悬嗓。因此自旋鎖是不適應(yīng)鎖占用時(shí)間長(zhǎng)的并發(fā)情況的污呼。

在 Java 中,AtomicInteger 類(lèi)有自旋的操作包竹,我們看一下代碼:

public final int getAndAddInt(Object o, long offset, int delta) {
    int v;
    do {
        v = getIntVolatile(o, offset);
    } while (!compareAndSwapInt(o, offset, v, v + delta));
    return v;
}

CAS 操作如果失敗就會(huì)一直循環(huán)獲取當(dāng)前 value 值然后重試燕酷。

另外自適應(yīng)自旋鎖也需要了解一下。

在JDK1.6又引入了自適應(yīng)自旋映企,這個(gè)就比較智能了悟狱,自旋時(shí)間不再固定,由前一次在同一個(gè)鎖上的自旋時(shí)間以及鎖的擁有者的狀態(tài)來(lái)決定堰氓。如果虛擬機(jī)認(rèn)為這次自旋也很有可能再次成功那就會(huì)次序較多的時(shí)間挤渐,如果自旋很少成功,那以后可能就直接省略掉自旋過(guò)程双絮,避免浪費(fèi)處理器資源浴麻。

分段鎖

分段鎖 是一種鎖的設(shè)計(jì),并不是具體的一種鎖囤攀。

分段鎖設(shè)計(jì)目的是將鎖的粒度進(jìn)一步細(xì)化软免,當(dāng)操作不需要更新整個(gè)數(shù)組的時(shí)候,就僅僅針對(duì)數(shù)組中的一項(xiàng)進(jìn)行加鎖操作焚挠。


在 Java 語(yǔ)言中 CurrentHashMap 底層就用了分段鎖膏萧,使用Segment,就可以進(jìn)行并發(fā)使用了蝌衔。

鎖升級(jí)(無(wú)鎖|偏向鎖|輕量級(jí)鎖|重量級(jí)鎖)

JDK1.6 為了提升性能減少獲得鎖和釋放鎖所帶來(lái)的消耗榛泛,引入了4種鎖的狀態(tài):無(wú)鎖、偏向鎖噩斟、輕量級(jí)鎖和重量級(jí)鎖曹锨,它會(huì)隨著多線程的競(jìng)爭(zhēng)情況逐漸升級(jí),但不能降級(jí)剃允。

無(wú)鎖

無(wú)鎖狀態(tài)其實(shí)就是上面講的樂(lè)觀鎖沛简,這里不再贅述齐鲤。

偏向鎖

Java偏向鎖(Biased Locking)是指它會(huì)偏向于第一個(gè)訪問(wèn)鎖的線程,如果在運(yùn)行過(guò)程中椒楣,只有一個(gè)線程訪問(wèn)加鎖的資源给郊,不存在多線程競(jìng)爭(zhēng)的情況,那么線程是不需要重復(fù)獲取鎖的撒顿,這種情況下丑罪,就會(huì)給線程加一個(gè)偏向鎖。

偏向鎖的實(shí)現(xiàn)是通過(guò)控制對(duì)象Mark Word的標(biāo)志位來(lái)實(shí)現(xiàn)的凤壁,如果當(dāng)前是可偏向狀態(tài)吩屹,需要進(jìn)一步判斷對(duì)象頭存儲(chǔ)的線程 ID 是否與當(dāng)前線程 ID 一致,如果一致直接進(jìn)入拧抖。

輕量級(jí)鎖

當(dāng)線程競(jìng)爭(zhēng)變得比較激烈時(shí)煤搜,偏向鎖就會(huì)升級(jí)為輕量級(jí)鎖,輕量級(jí)鎖認(rèn)為雖然競(jìng)爭(zhēng)是存在的唧席,但是理想情況下競(jìng)爭(zhēng)的程度很低擦盾,通過(guò)自旋方式等待上一個(gè)線程釋放鎖。

重量級(jí)鎖

如果線程并發(fā)進(jìn)一步加劇淌哟,線程的自旋超過(guò)了一定次數(shù)迹卢,或者一個(gè)線程持有鎖,一個(gè)線程在自旋徒仓,又來(lái)了第三個(gè)線程訪問(wèn)時(shí)(反正就是競(jìng)爭(zhēng)繼續(xù)加大了)腐碱,輕量級(jí)鎖就會(huì)膨脹為重量級(jí)鎖,重量級(jí)鎖會(huì)使除了此時(shí)擁有鎖的線程以外的線程都阻塞掉弛。

升級(jí)到重量級(jí)鎖其實(shí)就是互斥鎖了症见,一個(gè)線程拿到鎖,其余線程都會(huì)處于阻塞等待狀態(tài)殃饿。

在 Java 中谋作,synchronized 關(guān)鍵字內(nèi)部實(shí)現(xiàn)原理就是鎖升級(jí)的過(guò)程:無(wú)鎖 --> 偏向鎖 --> 輕量級(jí)鎖 --> 重量級(jí)鎖。這一過(guò)程在后續(xù)講解 synchronized 關(guān)鍵字的原理時(shí)會(huì)詳細(xì)介紹乎芳。

鎖優(yōu)化技術(shù)(鎖粗化遵蚜、鎖消除)

鎖粗化

鎖粗化就是將多個(gè)同步塊的數(shù)量減少,并將單個(gè)同步塊的作用范圍擴(kuò)大奈惑,本質(zhì)上就是將多次上鎖谬晕、解鎖的請(qǐng)求合并為一次同步請(qǐng)求。

舉個(gè)例子携取,一個(gè)循環(huán)體中有一個(gè)代碼同步塊,每次循環(huán)都會(huì)執(zhí)行加鎖解鎖操作帮孔。

private static final Object LOCK = new Object();

for(int i = 0;i < 100; i++) {
    synchronized(LOCK){
        // do some magic things
    }
}

經(jīng)過(guò)鎖粗化后就變成下面這個(gè)樣子了:

 synchronized(LOCK){
     for(int i = 0;i < 100; i++) {
        // do some magic things
    }
}

鎖消除

鎖消除是指虛擬機(jī)編譯器在運(yùn)行時(shí)檢測(cè)到了共享數(shù)據(jù)沒(méi)有競(jìng)爭(zhēng)的鎖雷滋,從而將這些鎖進(jìn)行消除不撑。

舉個(gè)例子讓大家更好理解。

public String test(String s1, String s2){
    StringBuffer stringBuffer = new StringBuffer();
    stringBuffer.append(s1);
    stringBuffer.append(s2);
    return stringBuffer.toString();
}

上面代碼中有一個(gè) test 方法晤斩,主要作用是將字符串 s1 和字符串 s2 串聯(lián)起來(lái)焕檬。

test 方法中三個(gè)變量s1, s2, stringBuffer, 它們都是局部變量澳泵,局部變量是在棧上的实愚,棧是線程私有的,所以就算有多個(gè)線程訪問(wèn) test 方法也是線程安全的兔辅。

我們都知道 StringBuffer 是線程安全的類(lèi)腊敲,append 方法是同步方法,但是 test 方法本來(lái)就是線程安全的维苔,為了提升效率碰辅,虛擬機(jī)幫我們消除了這些同步鎖,這個(gè)過(guò)程就被稱(chēng)為鎖消除介时。

StringBuffer.class

// append 是同步方法
public synchronized StringBuffer append(String str) {
    toStringCache = null;
    super.append(str);
    return this;
}

一張圖總結(jié):

前面講了 Java 語(yǔ)言中各種各種的鎖没宾,最后再通過(guò)六個(gè)問(wèn)題統(tǒng)一總結(jié)一下:



-- End --

原文鏈接:
https://mp.weixin.qq.com/s/2jxzNnyGWDFol4Q9R2a_Yg
作者:雷小帥

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市沸柔,隨后出現(xiàn)的幾起案子循衰,更是在濱河造成了極大的恐慌,老刑警劉巖褐澎,帶你破解...
    沈念sama閱讀 216,372評(píng)論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件会钝,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡乱凿,警方通過(guò)查閱死者的電腦和手機(jī)顽素,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,368評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)徒蟆,“玉大人胁出,你說(shuō)我怎么就攤上這事《紊螅” “怎么了全蝶?”我有些...
    開(kāi)封第一講書(shū)人閱讀 162,415評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)寺枉。 經(jīng)常有香客問(wèn)我抑淫,道長(zhǎng),這世上最難降的妖魔是什么姥闪? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,157評(píng)論 1 292
  • 正文 為了忘掉前任始苇,我火速辦了婚禮,結(jié)果婚禮上筐喳,老公的妹妹穿的比我還像新娘催式。我一直安慰自己函喉,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,171評(píng)論 6 388
  • 文/花漫 我一把揭開(kāi)白布荣月。 她就那樣靜靜地躺著管呵,像睡著了一般。 火紅的嫁衣襯著肌膚如雪哺窄。 梳的紋絲不亂的頭發(fā)上捐下,一...
    開(kāi)封第一講書(shū)人閱讀 51,125評(píng)論 1 297
  • 那天,我揣著相機(jī)與錄音萌业,去河邊找鬼坷襟。 笑死,一個(gè)胖子當(dāng)著我的面吹牛咽白,可吹牛的內(nèi)容都是我干的啤握。 我是一名探鬼主播,決...
    沈念sama閱讀 40,028評(píng)論 3 417
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼晶框,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼排抬!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起授段,我...
    開(kāi)封第一講書(shū)人閱讀 38,887評(píng)論 0 274
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤蹲蒲,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后侵贵,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體届搁,經(jīng)...
    沈念sama閱讀 45,310評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,533評(píng)論 2 332
  • 正文 我和宋清朗相戀三年窍育,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了卡睦。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,690評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡漱抓,死狀恐怖表锻,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情乞娄,我是刑警寧澤瞬逊,帶...
    沈念sama閱讀 35,411評(píng)論 5 343
  • 正文 年R本政府宣布,位于F島的核電站仪或,受9級(jí)特大地震影響确镊,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜范删,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,004評(píng)論 3 325
  • 文/蒙蒙 一蕾域、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧到旦,春花似錦束铭、人聲如沸廓块。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,659評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至昔汉,卻和暖如春懈万,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背靶病。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,812評(píng)論 1 268
  • 我被黑心中介騙來(lái)泰國(guó)打工会通, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人娄周。 一個(gè)月前我還...
    沈念sama閱讀 47,693評(píng)論 2 368
  • 正文 我出身青樓涕侈,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親煤辨。 傳聞我的和親對(duì)象是個(gè)殘疾皇子裳涛,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,577評(píng)論 2 353

推薦閱讀更多精彩內(nèi)容