萬字長(zhǎng)文,一文讀懂Java 中的各種鎖
Java 鎖分類
Java 中的鎖有很多,可以按照不同的功能、種類進(jìn)行分類,下面是我對(duì) Java 中一些常用鎖的分類,包括一些基本的概述

- 從線程是否需要對(duì)資源加鎖可以分為
悲觀鎖和樂觀鎖 - 從資源已被鎖定,線程是否阻塞可以分為
自旋鎖 - 從多個(gè)線程并發(fā)訪問資源,也就是 Synchronized 可以分為
無鎖、偏向鎖、輕量級(jí)鎖和重量級(jí)鎖 - 從鎖的公平性進(jìn)行區(qū)分,可以分為
公平鎖和非公平鎖 - 從根據(jù)鎖是否重復(fù)獲取可以分為
可重入鎖和不可重入鎖 - 從那個(gè)多個(gè)線程能否獲取同一把鎖分為
共享鎖和排他鎖
下面我們依次對(duì)各個(gè)鎖的分類進(jìn)行詳細(xì)闡述。
線程是否需要對(duì)資源加鎖
Java 按照是否對(duì)資源加鎖分為樂觀鎖和悲觀鎖,樂觀鎖和悲觀鎖并不是一種真實(shí)存在的鎖,而是一種設(shè)計(jì)思想,樂觀鎖和悲觀鎖對(duì)于理解 Java 多線程和數(shù)據(jù)庫(kù)來說至關(guān)重要,下面就來探討一下這兩種實(shí)現(xiàn)方式的區(qū)別和優(yōu)缺點(diǎn)
悲觀鎖
悲觀鎖是一種悲觀思想,它總認(rèn)為最壞的情況可能會(huì)出現(xiàn),它認(rèn)為數(shù)據(jù)很可能會(huì)被其他人所修改,所以悲觀鎖在持有數(shù)據(jù)的時(shí)候總會(huì)把資源 或者 數(shù)據(jù) 鎖住,這樣其他線程想要請(qǐng)求這個(gè)資源的時(shí)候就會(huì)阻塞,直到等到悲觀鎖把資源釋放為止。傳統(tǒng)的關(guān)系型數(shù)據(jù)庫(kù)里邊就用到了很多這種鎖機(jī)制,**比如行鎖,表鎖等,讀鎖,寫鎖等,都是在做操作之前先上鎖。**悲觀鎖的實(shí)現(xiàn)往往依靠數(shù)據(jù)庫(kù)本身的鎖功能實(shí)現(xiàn)。
Java 中的 Synchronized 和 ReentrantLock 等獨(dú)占鎖(排他鎖)也是一種悲觀鎖思想的實(shí)現(xiàn),因?yàn)?Synchronzied 和 ReetrantLock 不管是否持有資源,它都會(huì)嘗試去加鎖,生怕自己心愛的寶貝被別人拿走。
樂觀鎖
樂觀鎖的思想與悲觀鎖的思想相反,它總認(rèn)為資源和數(shù)據(jù)不會(huì)被別人所修改,所以讀取不會(huì)上鎖,但是樂觀鎖在進(jìn)行寫入操作的時(shí)候會(huì)判斷當(dāng)前數(shù)據(jù)是否被修改過(具體如何判斷我們下面再說)。樂觀鎖的實(shí)現(xiàn)方案一般來說有兩種:版本號(hào)機(jī)制 和 CAS實(shí)現(xiàn) 。樂觀鎖多適用于多讀的應(yīng)用類型,這樣可以提高吞吐量。
在Java中java.util.concurrent.atomic包下面的原子變量類就是使用了樂觀鎖的一種實(shí)現(xiàn)方式 CAS 實(shí)現(xiàn)的。
兩種鎖的使用場(chǎng)景
上面介紹了兩種鎖的基本概念,并提到了兩種鎖的適用場(chǎng)景,一般來說,悲觀鎖不僅會(huì)對(duì)寫操作加鎖還會(huì)對(duì)讀操作加鎖,一個(gè)典型的悲觀鎖調(diào)用:
select * from student where name="cxuan" for update
這條 sql 語句從 Student 表中選取 name = "cxuan" 的記錄并對(duì)其加鎖,那么其他寫操作再這個(gè)事務(wù)提交之前都不會(huì)對(duì)這條數(shù)據(jù)進(jìn)行操作,起到了獨(dú)占和排他的作用。
悲觀鎖因?yàn)閷?duì)讀寫都加鎖,所以它的性能比較低,對(duì)于現(xiàn)在互聯(lián)網(wǎng)提倡的三高(高性能、高可用、高并發(fā))來說,悲觀鎖的實(shí)現(xiàn)用的越來越少了,但是一般多讀的情況下還是需要使用悲觀鎖的,因?yàn)殡m然加鎖的性能比較低,但是也阻止了像樂觀鎖一樣,遇到寫不一致的情況下一直重試的時(shí)間。
相對(duì)而言,樂觀鎖用于讀多寫少的情況,即很少發(fā)生沖突的場(chǎng)景,這樣可以省去鎖的開銷,增加系統(tǒng)的吞吐量。
樂觀鎖的適用場(chǎng)景有很多,典型的比如說成本系統(tǒng),柜員要對(duì)一筆金額做修改,為了保證數(shù)據(jù)的準(zhǔn)確性和實(shí)效性,使用悲觀鎖鎖住某個(gè)數(shù)據(jù)后,再遇到其他需要修改數(shù)據(jù)的操作,那么此操作就無法完成金額的修改,對(duì)產(chǎn)品來說是災(zāi)難性的一刻,使用樂觀鎖的版本號(hào)機(jī)制能夠解決這個(gè)問題,我們下面說。
樂觀鎖的實(shí)現(xiàn)方式
樂觀鎖一般有兩種實(shí)現(xiàn)方式:采用版本號(hào)機(jī)制 和 CAS(Compare-and-Swap,即比較并替換)算法實(shí)現(xiàn)。
版本號(hào)機(jī)制
版本號(hào)機(jī)制是在數(shù)據(jù)表中加上一個(gè) version 字段來實(shí)現(xiàn)的,表示數(shù)據(jù)被修改的次數(shù),當(dāng)執(zhí)行寫操作并且寫入成功后,version = version + 1,當(dāng)線程A要更新數(shù)據(jù)時(shí),在讀取數(shù)據(jù)的同時(shí)也會(huì)讀取 version 值,在提交更新時(shí),若剛才讀取到的 version 值為當(dāng)前數(shù)據(jù)庫(kù)中的version值相等時(shí)才更新,否則重試更新操作,直到更新成功。
我們以上面的金融系統(tǒng)為例,來簡(jiǎn)述一下這個(gè)過程。

- 成本系統(tǒng)中有一個(gè)數(shù)據(jù)表,表中有兩個(gè)字段分別是
金額和version,金額的屬性是能夠?qū)崟r(shí)變化,而 version 表示的是金額每次發(fā)生變化的版本,一般的策略是,當(dāng)金額發(fā)生改變時(shí),version 采用遞增的策略每次都在上一個(gè)版本號(hào)的基礎(chǔ)上 + 1。 - 在了解了基本情況和基本信息之后,我們來看一下這個(gè)過程:公司收到回款后,需要把這筆錢放在金庫(kù)中,假如金庫(kù)中存有100 元錢
- 下面開啟事務(wù)一:當(dāng)男柜員執(zhí)行回款寫入操作前,他會(huì)先查看(讀)一下金庫(kù)中還有多少錢,此時(shí)讀到金庫(kù)中有 100 元,可以執(zhí)行寫操作,并把數(shù)據(jù)庫(kù)中的錢更新為 120 元,提交事務(wù),金庫(kù)中的錢由 100 -> 120,version的版本號(hào)由 0 -> 1。
- 開啟事務(wù)二:女柜員收到給員工發(fā)工資的請(qǐng)求后,需要先執(zhí)行讀請(qǐng)求,查看金庫(kù)中的錢還有多少,此時(shí)的版本號(hào)是多少,然后從金庫(kù)中取出員工的工資進(jìn)行發(fā)放,提交事務(wù),成功后版本 + 1,此時(shí)版本由 1 -> 2。
上面兩種情況是最樂觀的情況,上面的兩個(gè)事務(wù)都是順序執(zhí)行的,也就是事務(wù)一和事務(wù)二互不干擾,那么事務(wù)要并行執(zhí)行會(huì)如何呢?

事務(wù)一開啟,男柜員先執(zhí)行讀操作,取出金額和版本號(hào),執(zhí)行寫操作
begin
update 表 set 金額 = 120,version = version + 1 where 金額 = 100 and version = 0此時(shí)金額改為 120,版本號(hào)為1,事務(wù)還沒有提交
事務(wù)二開啟,女柜員先執(zhí)行讀操作,取出金額和版本號(hào),執(zhí)行寫操作
begin
update 表 set 金額 = 50,version = version + 1 where 金額 = 100 and version = 0此時(shí)金額改為 50,版本號(hào)變?yōu)?1,事務(wù)未提交
現(xiàn)在提交事務(wù)一,金額改為 120,版本變?yōu)?,提交事務(wù)。理想情況下應(yīng)該變?yōu)?金額 = 50,版本號(hào) = 2,但是實(shí)際上事務(wù)二 的更新是建立在金額為 100 和 版本號(hào)為 0 的基礎(chǔ)上的,所以事務(wù)二不會(huì)提交成功,應(yīng)該重新讀取金額和版本號(hào),再次進(jìn)行寫操作。
這樣,就避免了女柜員 用基于 version = 0 的舊數(shù)據(jù)修改的結(jié)果覆蓋男操作員操作結(jié)果的可能。
CAS 算法
省略代碼,完整代碼請(qǐng)參照 看完你就應(yīng)該能明白的悲觀鎖和樂觀鎖
CAS 即 compare and swap(比較與交換),是一種有名的無鎖算法。即不使用鎖的情況下實(shí)現(xiàn)多線程之間的變量同步,也就是在沒有線程被阻塞的情況下實(shí)現(xiàn)變量的同步,所以也叫非阻塞同步(Non-blocking Synchronization
Java 從 JDK1.5 開始支持,java.util.concurrent 包里提供了很多面向并發(fā)編程的類,也提供了 CAS 算法的支持,一些以 Atomic 為開頭的一些原子類都使用 CAS 作為其實(shí)現(xiàn)方式。使用這些類在多核 CPU 的機(jī)器上會(huì)有比較好的性能。
如果要保證它們的原子性,必須進(jìn)行加鎖,使用 Synchronzied 或者 ReentrantLock,我們前面介紹它們是悲觀鎖的實(shí)現(xiàn),我們現(xiàn)在討論的是樂觀鎖,那么用哪種方式保證它們的原子性呢?請(qǐng)繼續(xù)往下看
CAS 中涉及三個(gè)要素:
- 需要讀寫的內(nèi)存值 V
- 進(jìn)行比較的值 A
- 擬寫入的新值 B
當(dāng)且僅當(dāng)預(yù)期值A(chǔ)和內(nèi)存值V相同時(shí),將內(nèi)存值V修改為B,否則什么都不做。
我們以 java.util.concurrent 中的AtomicInteger 為例,看一下在不用鎖的情況下是如何保證線程安全的
public class AtomicCounter {
private AtomicInteger integer = new AtomicInteger();
public AtomicInteger getInteger() {
return integer;
}
public void setInteger(AtomicInteger integer) {
this.integer = integer;
}
public void increment(){
integer.incrementAndGet();
}
public void decrement(){
integer.decrementAndGet();
}
}
public class AtomicProducer extends Thread{
private AtomicCounter atomicCounter;
public AtomicProducer(AtomicCounter atomicCounter){
this.atomicCounter = atomicCounter;
}
@Override
public void run() {
for(int j = 0; j < AtomicTest.LOOP; j++) {
System.out.println("producer : " + atomicCounter.getInteger());
atomicCounter.increment();
}
}
}
public class AtomicConsumer extends Thread{
private AtomicCounter atomicCounter;
public AtomicConsumer(AtomicCounter atomicCounter){
this.atomicCounter = atomicCounter;
}
@Override
public void run() {
for(int j = 0; j < AtomicTest.LOOP; j++) {
System.out.println("consumer : " + atomicCounter.getInteger());
atomicCounter.decrement();
}
}
}
public class AtomicTest {
final static int LOOP = 10000;
public static void main(String[] args) throws InterruptedException {
AtomicCounter counter = new AtomicCounter();
AtomicProducer producer = new AtomicProducer(counter);
AtomicConsumer consumer = new AtomicConsumer(counter);
producer.start();
consumer.start();
producer.join();
consumer.join();
System.out.println(counter.getInteger());
}
}
經(jīng)測(cè)試可得,不管循環(huán)多少次最后的結(jié)果都是0,也就是多線程并行的情況下,使用 AtomicInteger 可以保證線程安全性。incrementAndGet 和 decrementAndGet 都是原子性操作。
樂觀鎖的缺點(diǎn)
任何事情都是有利也有弊,軟件行業(yè)沒有完美的解決方案只有最優(yōu)的解決方案,所以樂觀鎖也有它的弱點(diǎn)和缺陷:
ABA 問題
ABA 問題說的是,如果一個(gè)變量第一次讀取的值是 A,準(zhǔn)備好需要對(duì) A 進(jìn)行寫操作的時(shí)候,發(fā)現(xiàn)值還是 A,那么這種情況下,能認(rèn)為 A 的值沒有被改變過嗎?可以是由 A -> B -> A 的這種情況,但是 AtomicInteger 卻不會(huì)這么認(rèn)為,它只相信它看到的,它看到的是什么就是什么。
JDK 1.5 以后的 AtomicStampedReference類就提供了此種能力,其中的 compareAndSet 方法就是首先檢查當(dāng)前引用是否等于預(yù)期引用,并且當(dāng)前標(biāo)志是否等于預(yù)期標(biāo)志,如果全部相等,則以原子方式將該引用和該標(biāo)志的值設(shè)置為給定的更新值。
也可以采用CAS的一個(gè)變種DCAS來解決這個(gè)問題。DCAS,是對(duì)于每一個(gè)V增加一個(gè)引用的表示修改次數(shù)的標(biāo)記符。對(duì)于每個(gè)V,如果引用修改了一次,這個(gè)計(jì)數(shù)器就加1。然后在這個(gè)變量需要update的時(shí)候,就同時(shí)檢查變量的值和計(jì)數(shù)器的值。
循環(huán)開銷大
我們知道樂觀鎖在進(jìn)行寫操作的時(shí)候會(huì)判斷是否能夠?qū)懭氤晒Γ绻麑懭氩怀晒⒂|發(fā)等待 -> 重試機(jī)制,這種情況是一個(gè)自旋鎖,簡(jiǎn)單來說就是適用于短期內(nèi)獲取不到,進(jìn)行等待重試的鎖,它不適用于長(zhǎng)期獲取不到鎖的情況,另外,自旋循環(huán)對(duì)于性能開銷比較大。
CAS與synchronized的使用情景
簡(jiǎn)單的來說 CAS 適用于寫比較少的情況下(多讀場(chǎng)景,沖突一般較少),synchronized 適用于寫比較多的情況下(多寫場(chǎng)景,沖突一般較多)
- 對(duì)于資源競(jìng)爭(zhēng)較少(線程沖突較輕)的情況,使用 Synchronized 同步鎖進(jìn)行線程阻塞和喚醒切換以及用戶態(tài)內(nèi)核態(tài)間的切換操作額外浪費(fèi)消耗 cpu 資源;而 CAS 基于硬件實(shí)現(xiàn),不需要進(jìn)入內(nèi)核,不需要切換線程,操作自旋幾率較少,因此可以獲得更高的性能。
- 對(duì)于資源競(jìng)爭(zhēng)嚴(yán)重(線程沖突嚴(yán)重)的情況,CAS 自旋的概率會(huì)比較大,從而浪費(fèi)更多的 CPU 資源,效率低于 synchronized。
資源已被鎖定,線程是否阻塞
自旋鎖的提出背景
由于在多處理器環(huán)境中某些資源的有限性,有時(shí)需要互斥訪問(mutual exclusion),這時(shí)候就需要引入鎖的概念,只有獲取了鎖的線程才能夠?qū)Y源進(jìn)行訪問,由于多線程的核心是CPU的時(shí)間分片,所以同一時(shí)刻只能有一個(gè)線程獲取到鎖。那么就面臨一個(gè)問題,那么沒有獲取到鎖的線程應(yīng)該怎么辦?
通常有兩種處理方式:一種是沒有獲取到鎖的線程就一直循環(huán)等待判斷該資源是否已經(jīng)釋放鎖,這種鎖叫做自旋鎖,它不用將線程阻塞起來(NON-BLOCKING);還有一種處理方式就是把自己阻塞起來,等待重新調(diào)度請(qǐng)求,這種叫做互斥鎖。
什么是自旋鎖
自旋鎖的定義:當(dāng)一個(gè)線程嘗試去獲取某一把鎖的時(shí)候,如果這個(gè)鎖此時(shí)已經(jīng)被別人獲取(占用),那么此線程就無法獲取到這把鎖,該線程將會(huì)等待,間隔一段時(shí)間后會(huì)再次嘗試獲取。這種采用循環(huán)加鎖 -> 等待的機(jī)制被稱為自旋鎖(spinlock)。

自旋鎖的原理
自旋鎖的原理比較簡(jiǎn)單,如果持有鎖的線程能在短時(shí)間內(nèi)釋放鎖資源,那么那些等待競(jìng)爭(zhēng)鎖的線程就不需要做內(nèi)核態(tài)和用戶態(tài)之間的切換進(jìn)入阻塞狀態(tài),它們只需要等一等(自旋),等到持有鎖的線程釋放鎖之后即可獲取,這樣就避免了用戶進(jìn)程和內(nèi)核切換的消耗。
因?yàn)樽孕i避免了操作系統(tǒng)進(jìn)程調(diào)度和線程切換,所以自旋鎖通常適用在時(shí)間比較短的情況下。由于這個(gè)原因,操作系統(tǒng)的內(nèi)核經(jīng)常使用自旋鎖。但是,如果長(zhǎng)時(shí)間上鎖的話,自旋鎖會(huì)非常耗費(fèi)性能,它阻止了其他線程的運(yùn)行和調(diào)度。線程持有鎖的時(shí)間越長(zhǎng),則持有該鎖的線程將被 OS(Operating System) 調(diào)度程序中斷的風(fēng)險(xiǎn)越大。如果發(fā)生中斷情況,那么其他線程將保持旋轉(zhuǎn)狀態(tài)(反復(fù)嘗試獲取鎖),而持有該鎖的線程并不打算釋放鎖,這樣導(dǎo)致的是結(jié)果是無限期推遲,直到持有鎖的線程可以完成并釋放它為止。
解決上面這種情況一個(gè)很好的方式是給自旋鎖設(shè)定一個(gè)自旋時(shí)間,等時(shí)間一到立即釋放自旋鎖。自旋鎖的目的是占著CPU資源不進(jìn)行釋放,等到獲取鎖立即進(jìn)行處理。但是如何去選擇自旋時(shí)間呢?如果自旋執(zhí)行時(shí)間太長(zhǎng),會(huì)有大量的線程處于自旋狀態(tài)占用 CPU 資源,進(jìn)而會(huì)影響整體系統(tǒng)的性能。因此自旋的周期選的額外重要!JDK在1.6 引入了適應(yīng)性自旋鎖,適應(yīng)性自旋鎖意味著自旋時(shí)間不是固定的了,而是由前一次在同一個(gè)鎖上的自旋時(shí)間以及鎖擁有的狀態(tài)來決定,基本認(rèn)為一個(gè)線程上下文切換的時(shí)間是最佳的一個(gè)時(shí)間。
自旋鎖的優(yōu)缺點(diǎn)
自旋鎖盡可能的減少線程的阻塞,這對(duì)于鎖的競(jìng)爭(zhēng)不激烈,且占用鎖時(shí)間非常短的代碼塊來說性能能大幅度的提升,因?yàn)樽孕南臅?huì)小于線程阻塞掛起再喚醒的操作的消耗,這些操作會(huì)導(dǎo)致線程發(fā)生兩次上下文切換!
但是如果鎖的競(jìng)爭(zhēng)激烈,或者持有鎖的線程需要長(zhǎng)時(shí)間占用鎖執(zhí)行同步塊,這時(shí)候就不適合使用自旋鎖了,因?yàn)樽孕i在獲取鎖前一直都是占用 cpu 做無用功,占著 XX 不 XX,同時(shí)有大量線程在競(jìng)爭(zhēng)一個(gè)鎖,會(huì)導(dǎo)致獲取鎖的時(shí)間很長(zhǎng),線程自旋的消耗大于線程阻塞掛起操作的消耗,其它需要 cpu 的線程又不能獲取到 cpu,造成 cpu 的浪費(fèi)。所以這種情況下我們要關(guān)閉自旋鎖。
自旋鎖的實(shí)現(xiàn)
下面我們用Java 代碼來實(shí)現(xiàn)一個(gè)簡(jiǎn)單的自旋鎖
public class SpinLockTest {
private AtomicBoolean available = new AtomicBoolean(false);
public void lock(){
// 循環(huán)檢測(cè)嘗試獲取鎖
while (!tryLock()){
// doSomething...
}
}
public boolean tryLock(){
// 嘗試獲取鎖,成功返回true,失敗返回false
return available.compareAndSet(false,true);
}
public void unLock(){
if(!available.compareAndSet(true,false)){
throw new RuntimeException("釋放鎖失敗");
}
}
}
這種簡(jiǎn)單的自旋鎖有一個(gè)問題:無法保證多線程競(jìng)爭(zhēng)的公平性。對(duì)于上面的 SpinlockTest,當(dāng)多個(gè)線程想要獲取鎖時(shí),誰最先將available設(shè)為false誰就能最先獲得鎖,這可能會(huì)造成某些線程一直都未獲取到鎖造成線程饑餓。就像我們下課后蜂擁的跑向食堂,下班后蜂擁地?cái)D向地鐵,通常我們會(huì)采取排隊(duì)的方式解決這樣的問題,類似地,我們把這種鎖叫排隊(duì)自旋鎖(QueuedSpinlock)。計(jì)算機(jī)科學(xué)家們使用了各種方式來實(shí)現(xiàn)排隊(duì)自旋鎖,如TicketLock,MCSLock,CLHLock。接下來我們分別對(duì)這幾種鎖做個(gè)大致的介紹。
TicketLock
在計(jì)算機(jī)科學(xué)領(lǐng)域中,TicketLock 是一種同步機(jī)制或鎖定算法,它是一種自旋鎖,它使用ticket 來控制線程執(zhí)行順序。
就像票據(jù)隊(duì)列管理系統(tǒng)一樣。面包店或者服務(wù)機(jī)構(gòu)(例如銀行)都會(huì)使用這種方式來為每個(gè)先到達(dá)的顧客記錄其到達(dá)的順序,而不用每次都進(jìn)行排隊(duì)。通常,這種地點(diǎn)都會(huì)有一個(gè)分配器(叫號(hào)器,掛號(hào)器等等都行),先到的人需要在這個(gè)機(jī)器上取出自己現(xiàn)在排隊(duì)的號(hào)碼,這個(gè)號(hào)碼是按照自增的順序進(jìn)行的,旁邊還會(huì)有一個(gè)標(biāo)牌顯示的是正在服務(wù)的標(biāo)志,這通常是代表目前正在服務(wù)的隊(duì)列號(hào),當(dāng)前的號(hào)碼完成服務(wù)后,標(biāo)志牌會(huì)顯示下一個(gè)號(hào)碼可以去服務(wù)了。
像上面系統(tǒng)一樣,TicketLock 是基于先進(jìn)先出(FIFO) 隊(duì)列的機(jī)制。它增加了鎖的公平性,其設(shè)計(jì)原則如下:TicketLock 中有兩個(gè) int 類型的數(shù)值,開始都是0,第一個(gè)值是隊(duì)列ticket(隊(duì)列票據(jù)), 第二個(gè)值是 出隊(duì)(票據(jù))。隊(duì)列票據(jù)是線程在隊(duì)列中的位置,而出隊(duì)票據(jù)是現(xiàn)在持有鎖的票證的隊(duì)列位置。可能有點(diǎn)模糊不清,簡(jiǎn)單來說,就是隊(duì)列票據(jù)是你取票號(hào)的位置,出隊(duì)票據(jù)是你距離叫號(hào)的位置。現(xiàn)在應(yīng)該明白一些了吧。
當(dāng)叫號(hào)叫到你的時(shí)候,不能有相同的號(hào)碼同時(shí)辦業(yè)務(wù),必須只有一個(gè)人可以去辦,辦完后,叫號(hào)機(jī)叫到下一個(gè)人,這就叫做原子性。你在辦業(yè)務(wù)的時(shí)候不能被其他人所干擾,而且不可能會(huì)有兩個(gè)持有相同號(hào)碼的人去同時(shí)辦業(yè)務(wù)。然后,下一個(gè)人看自己的號(hào)是否和叫到的號(hào)碼保持一致,如果一致的話,那么就輪到你去辦業(yè)務(wù),否則只能繼續(xù)等待。上面這個(gè)流程的關(guān)鍵點(diǎn)在于,每個(gè)辦業(yè)務(wù)的人在辦完業(yè)務(wù)之后,他必須丟棄自己的號(hào)碼,叫號(hào)機(jī)才能繼續(xù)叫到下面的人,如果這個(gè)人沒有丟棄這個(gè)號(hào)碼,那么其他人只能繼續(xù)等待。下面來實(shí)現(xiàn)一下這個(gè)票據(jù)排隊(duì)方案
public class TicketLock {
// 隊(duì)列票據(jù)(當(dāng)前排隊(duì)號(hào)碼)
private AtomicInteger queueNum = new AtomicInteger();
// 出隊(duì)票據(jù)(當(dāng)前需等待號(hào)碼)
private AtomicInteger dueueNum = new AtomicInteger();
// 獲取鎖:如果獲取成功,返回當(dāng)前線程的排隊(duì)號(hào)
public int lock(){
int currentTicketNum = dueueNum.incrementAndGet();
while (currentTicketNum != queueNum.get()){
// doSomething...
}
return currentTicketNum;
}
// 釋放鎖:傳入當(dāng)前排隊(duì)的號(hào)碼
public void unLock(int ticketNum){
queueNum.compareAndSet(ticketNum,ticketNum + 1);
}
}
每次叫號(hào)機(jī)在叫號(hào)的時(shí)候,都會(huì)判斷自己是不是被叫的號(hào),并且每個(gè)人在辦完業(yè)務(wù)的時(shí)候,叫號(hào)機(jī)根據(jù)在當(dāng)前號(hào)碼的基礎(chǔ)上 + 1,讓隊(duì)列繼續(xù)往前走。
但是上面這個(gè)設(shè)計(jì)是有問題的,因?yàn)楂@得自己的號(hào)碼之后,是可以對(duì)號(hào)碼進(jìn)行更改的,這就造成系統(tǒng)紊亂,鎖不能及時(shí)釋放。這時(shí)候就需要有一個(gè)能確保每個(gè)人按會(huì)著自己號(hào)碼排隊(duì)辦業(yè)務(wù)的角色,在得知這一點(diǎn)之后,我們重新設(shè)計(jì)一下這個(gè)邏輯
public class TicketLock2 {
// 隊(duì)列票據(jù)(當(dāng)前排隊(duì)號(hào)碼)
private AtomicInteger queueNum = new AtomicInteger();
// 出隊(duì)票據(jù)(當(dāng)前需等待號(hào)碼)
private AtomicInteger dueueNum = new AtomicInteger();
private ThreadLocal ticketLocal = new ThreadLocal<>();
public void lock(){
int currentTicketNum = dueueNum.incrementAndGet();
// 獲取鎖的時(shí)候,將當(dāng)前線程的排隊(duì)號(hào)保存起來
ticketLocal.set(currentTicketNum);
while (currentTicketNum != queueNum.get()){
// doSomething...
}
}
// 釋放鎖:從排隊(duì)緩沖池中取
public void unLock(){
Integer currentTicket = ticketLocal.get();
queueNum.compareAndSet(currentTicket,currentTicket + 1);
}
}
這次就不再需要返回值,辦業(yè)務(wù)的時(shí)候,要將當(dāng)前的這一個(gè)號(hào)碼緩存起來,在辦完業(yè)務(wù)后,需要釋放緩存的這條票據(jù)。
缺點(diǎn)
TicketLock 雖然解決了公平性的問題,但是多處理器系統(tǒng)上,每個(gè)進(jìn)程/線程占用的處理器都在讀寫同一個(gè)變量queueNum ,每次讀寫操作都必須在多個(gè)處理器緩存之間進(jìn)行緩存同步,這會(huì)導(dǎo)致繁重的系統(tǒng)總線和內(nèi)存的流量,大大降低系統(tǒng)整體的性能。
為了解決這個(gè)問題,MCSLock 和 CLHLock 應(yīng)運(yùn)而生。
CLHLock
上面說到TicketLock 是基于隊(duì)列的,那么 CLHLock 就是基于鏈表設(shè)計(jì)的,CLH的發(fā)明人是:Craig,Landin and Hagersten,用它們各自的字母開頭命名。CLH 是一種基于鏈表的可擴(kuò)展,高性能,公平的自旋鎖,申請(qǐng)線程只能在本地變量上自旋,它會(huì)不斷輪詢前驅(qū)的狀態(tài),如果發(fā)現(xiàn)前驅(qū)釋放了鎖就結(jié)束自旋。
public class CLHLock {
public static class CLHNode{
private volatile boolean isLocked = true;
}
// 尾部節(jié)點(diǎn)
private volatile CLHNode tail;
private static final ThreadLocal LOCAL = new ThreadLocal<>();
private static final AtomicReferenceFieldUpdater UPDATER =
AtomicReferenceFieldUpdater.newUpdater(CLHLock.class,CLHNode.class,"tail");
public void lock(){
// 新建節(jié)點(diǎn)并將節(jié)點(diǎn)與當(dāng)前線程保存起來
CLHNode node = new CLHNode();
LOCAL.set(node);
// 將新建的節(jié)點(diǎn)設(shè)置為尾部節(jié)點(diǎn),并返回舊的節(jié)點(diǎn)(原子操作),這里舊的節(jié)點(diǎn)實(shí)際上就是當(dāng)前節(jié)點(diǎn)的前驅(qū)節(jié)點(diǎn)
CLHNode preNode = UPDATER.getAndSet(this,node);
if(preNode != null){
// 前驅(qū)節(jié)點(diǎn)不為null表示當(dāng)鎖被其他線程占用,通過不斷輪詢判斷前驅(qū)節(jié)點(diǎn)的鎖標(biāo)志位等待前驅(qū)節(jié)點(diǎn)釋放鎖
while (preNode.isLocked){
}
preNode = null;
LOCAL.set(node);
}
// 如果不存在前驅(qū)節(jié)點(diǎn),表示該鎖沒有被其他線程占用,則當(dāng)前線程獲得鎖
}
public void unlock() {
// 獲取當(dāng)前線程對(duì)應(yīng)的節(jié)點(diǎn)
CLHNode node = LOCAL.get();
// 如果tail節(jié)點(diǎn)等于node,則將tail節(jié)點(diǎn)更新為null,同時(shí)將node的lock狀態(tài)職位false,表示當(dāng)前線程釋放了鎖
if (!UPDATER.compareAndSet(this, node, null)) {
node.isLocked = false;
}
node = null;
}
}
MCSLock
MCS Spinlock 是一種基于鏈表的可擴(kuò)展、高性能、公平的自旋鎖,申請(qǐng)線程只在本地變量上自旋,直接前驅(qū)負(fù)責(zé)通知其結(jié)束自旋,從而極大地減少了不必要的處理器緩存同步的次數(shù),降低了總線和內(nèi)存的開銷。MCS 來自于其發(fā)明人名字的首字母:John Mellor-Crummey 和 Michael Scott。
public class MCSLock {
public static class MCSNode {
volatile MCSNode next;
volatile boolean isLocked = true;
}
private static final ThreadLocal NODE = new ThreadLocal<>();
// 隊(duì)列
@SuppressWarnings("unused")
private volatile MCSNode queue;
private static final AtomicReferenceFieldUpdater UPDATE =
AtomicReferenceFieldUpdater.newUpdater(MCSLock.class,MCSNode.class,"queue");
public void lock(){
// 創(chuàng)建節(jié)點(diǎn)并保存到ThreadLocal中
MCSNode currentNode = new MCSNode();
NODE.set(currentNode);
// 將queue設(shè)置為當(dāng)前節(jié)點(diǎn),并且返回之前的節(jié)點(diǎn)
MCSNode preNode = UPDATE.getAndSet(this, currentNode);
if (preNode != null) {
// 如果之前節(jié)點(diǎn)不為null,表示鎖已經(jīng)被其他線程持有
preNode.next = currentNode;
// 循環(huán)判斷,直到當(dāng)前節(jié)點(diǎn)的鎖標(biāo)志位為false
while (currentNode.isLocked) {
}
}
}
public void unlock() {
MCSNode currentNode = NODE.get();
// next為null表示沒有正在等待獲取鎖的線程
if (currentNode.next == null) {
// 更新狀態(tài)并設(shè)置queue為null
if (UPDATE.compareAndSet(this, currentNode, null)) {
// 如果成功了,表示queue==currentNode,即當(dāng)前節(jié)點(diǎn)后面沒有節(jié)點(diǎn)了
return;
} else {
// 如果不成功,表示queue!=currentNode,即當(dāng)前節(jié)點(diǎn)后面多了一個(gè)節(jié)點(diǎn),表示有線程在等待
// 如果當(dāng)前節(jié)點(diǎn)的后續(xù)節(jié)點(diǎn)為null,則需要等待其不為null(參考加鎖方法)
while (currentNode.next == null) {
}
}
} else {
// 如果不為null,表示有線程在等待獲取鎖,此時(shí)將等待線程對(duì)應(yīng)的節(jié)點(diǎn)鎖狀態(tài)更新為false,同時(shí)將當(dāng)前線程的后繼節(jié)點(diǎn)設(shè)為null
currentNode.next.isLocked = false;
currentNode.next = null;
}
}
}
CLHLock 和 MCSLock
- 都是基于鏈表,不同的是CLHLock是基于隱式鏈表,沒有真正的后續(xù)節(jié)點(diǎn)屬性,MCSLock是顯示鏈表,有一個(gè)指向后續(xù)節(jié)點(diǎn)的屬性。
- 將獲取鎖的線程狀態(tài)借助節(jié)點(diǎn)(node)保存,每個(gè)線程都有一份獨(dú)立的節(jié)點(diǎn),這樣就解決了TicketLock多處理器緩存同步的問題。
多個(gè)線程并發(fā)訪問資源
鎖狀態(tài)的分類
Java 語言專門針對(duì) synchronized 關(guān)鍵字設(shè)置了四種狀態(tài),它們分別是:無鎖、偏向鎖、輕量級(jí)鎖和重量級(jí)鎖,但是在了解這些鎖之前還需要先了解一下 Java 對(duì)象頭和 Monitor。
Java 對(duì)象頭
我們知道 synchronized 是悲觀鎖,在操作同步之前需要給資源加鎖,這把鎖就是對(duì)象頭里面的,而Java 對(duì)象頭又是什么呢?我們以 Hotspot 虛擬機(jī)為例,Hopspot 對(duì)象頭主要包括兩部分?jǐn)?shù)據(jù):Mark Word(標(biāo)記字段) 和 class Pointer(類型指針)。
Mark Word:默認(rèn)存儲(chǔ)對(duì)象的HashCode,分代年齡和鎖標(biāo)志位信息。這些信息都是與對(duì)象自身定義無關(guān)的數(shù)據(jù),所以Mark Word被設(shè)計(jì)成一個(gè)非固定的數(shù)據(jù)結(jié)構(gòu)以便在極小的空間內(nèi)存存儲(chǔ)盡量多的數(shù)據(jù)。它會(huì)根據(jù)對(duì)象的狀態(tài)復(fù)用自己的存儲(chǔ)空間,也就是說在運(yùn)行期間Mark Word里存儲(chǔ)的數(shù)據(jù)會(huì)隨著鎖標(biāo)志位的變化而變化。
class Point:對(duì)象指向它的類元數(shù)據(jù)的指針,虛擬機(jī)通過這個(gè)指針來確定這個(gè)對(duì)象是哪個(gè)類的實(shí)例。
在32位虛擬機(jī)和64位虛擬機(jī)的 Mark Word 所占用的字節(jié)大小不一樣,32位虛擬機(jī)的 Mark Word 和 class Pointer 分別占用 32bits 的字節(jié),而 64位虛擬機(jī)的 Mark Word 和 class Pointer 占用了64bits 的字節(jié),下面我們以 32位虛擬機(jī)為例,來看一下其 Mark Word 的字節(jié)具體是如何分配的


用中文翻譯過來就是

- 無狀態(tài)也就是
無鎖的時(shí)候,對(duì)象頭開辟 25bit 的空間用來存儲(chǔ)對(duì)象的 hashcode ,4bit 用于存放分代年齡,1bit 用來存放是否偏向鎖的標(biāo)識(shí)位,2bit 用來存放鎖標(biāo)識(shí)位為01 偏向鎖中劃分更細(xì),還是開辟25bit 的空間,其中23bit 用來存放線程ID,2bit 用來存放 epoch,4bit 存放分代年齡,1bit 存放是否偏向鎖標(biāo)識(shí), 0表示無鎖,1表示偏向鎖,鎖的標(biāo)識(shí)位還是01輕量級(jí)鎖中直接開辟 30bit 的空間存放指向棧中鎖記錄的指針,2bit 存放鎖的標(biāo)志位,其標(biāo)志位為00重量級(jí)鎖中和輕量級(jí)鎖一樣,30bit 的空間用來存放指向重量級(jí)鎖的指針,2bit 存放鎖的標(biāo)識(shí)位,為11GC標(biāo)記開辟30bit 的內(nèi)存空間卻沒有占用,2bit 空間存放鎖標(biāo)志位為11。
其中無鎖和偏向鎖的鎖標(biāo)志位都是01,只是在前面的1bit區(qū)分了這是無鎖狀態(tài)還是偏向鎖狀態(tài)。
關(guān)于為什么這么分配的內(nèi)存,我們可以從 OpenJDK 中的markOop.hpp類中的枚舉窺出端倪

來解釋一下
- age_bits 就是我們說的分代回收的標(biāo)識(shí),占用4字節(jié)
- lock_bits 是鎖的標(biāo)志位,占用2個(gè)字節(jié)
- biased_lock_bits 是是否偏向鎖的標(biāo)識(shí),占用1個(gè)字節(jié)
- max_hash_bits 是針對(duì)無鎖計(jì)算的hashcode 占用字節(jié)數(shù)量,如果是32位虛擬機(jī),就是 32 - 4 - 2 -1 = 25 byte,如果是64 位虛擬機(jī),64 - 4 - 2 - 1 = 57 byte,但是會(huì)有 25 字節(jié)未使用,所以64位的 hashcode 占用 31 byte
- hash_bits 是針對(duì) 64 位虛擬機(jī)來說,如果最大字節(jié)數(shù)大于 31,則取31,否則取真實(shí)的字節(jié)數(shù)
- cms_bits 我覺得應(yīng)該是不是64位虛擬機(jī)就占用 0 byte,是64位就占用 1byte
- epoch_bits 就是 epoch 所占用的字節(jié)大小,2字節(jié)。
Synchronized鎖
synchronized用的鎖是存在Java對(duì)象頭里的。
JVM基于進(jìn)入和退出 Monitor 對(duì)象來實(shí)現(xiàn)方法同步和代碼塊同步。代碼塊同步是使用 monitorenter 和 monitorexit 指令實(shí)現(xiàn)的,monitorenter 指令是在編譯后插入到同步代碼塊的開始位置,而 monitorexit 是插入到方法結(jié)束處和異常處。任何對(duì)象都有一個(gè) monitor 與之關(guān)聯(lián),當(dāng)且一個(gè) monitor 被持有后,它將處于鎖定狀態(tài)。
根據(jù)虛擬機(jī)規(guī)范的要求,在執(zhí)行 monitorenter 指令時(shí),首先要去嘗試獲取對(duì)象的鎖,如果這個(gè)對(duì)象沒被鎖定,或者當(dāng)前線程已經(jīng)擁有了那個(gè)對(duì)象的鎖,把鎖的計(jì)數(shù)器加1,相應(yīng)地,在執(zhí)行 monitorexit 指令時(shí)會(huì)將鎖計(jì)數(shù)器減1,當(dāng)計(jì)數(shù)器被減到0時(shí),鎖就釋放了。如果獲取對(duì)象鎖失敗了,那當(dāng)前線程就要阻塞等待,直到對(duì)象鎖被另一個(gè)線程釋放為止。
Monitor
Synchronized是通過對(duì)象內(nèi)部的一個(gè)叫做監(jiān)視器鎖(monitor)來實(shí)現(xiàn)的,監(jiān)視器鎖本質(zhì)又是依賴于底層的操作系統(tǒng)的 Mutex Lock(互斥鎖)來實(shí)現(xiàn)的。而操作系統(tǒng)實(shí)現(xiàn)線程之間的切換需要從用戶態(tài)轉(zhuǎn)換到核心態(tài),這個(gè)成本非常高,狀態(tài)之間的轉(zhuǎn)換需要相對(duì)比較長(zhǎng)的時(shí)間,這就是為什么 Synchronized 效率低的原因。因此,這種依賴于操作系統(tǒng) Mutex Lock 所實(shí)現(xiàn)的鎖我們稱之為重量級(jí)鎖。
Java SE 1.6為了減少獲得鎖和釋放鎖帶來的性能消耗,引入了偏向鎖和輕量級(jí)鎖:鎖一共有4種狀態(tài),級(jí)別從低到高依次是:無鎖狀態(tài)、偏向鎖狀態(tài)、輕量級(jí)鎖狀態(tài)和重量級(jí)鎖狀態(tài)。鎖可以升級(jí)但不能降級(jí)。
所以鎖的狀態(tài)總共有四種:無鎖狀態(tài)、偏向鎖、輕量級(jí)鎖和重量級(jí)鎖。隨著鎖的競(jìng)爭(zhēng),鎖可以從偏向鎖升級(jí)到輕量級(jí)鎖,再升級(jí)的重量級(jí)鎖(但是鎖的升級(jí)是單向的,也就是說只能從低到高升級(jí),不會(huì)出現(xiàn)鎖的降級(jí))。JDK 1.6中默認(rèn)是開啟偏向鎖和輕量級(jí)鎖的,我們也可以通過-XX:-UseBiasedLocking=false來禁用偏向鎖。
鎖的分類及其解釋
先來個(gè)大體的流程圖來感受一下這個(gè)過程,然后下面我們?cè)俜珠_來說

無鎖
無鎖狀態(tài),無鎖即沒有對(duì)資源進(jìn)行鎖定,所有的線程都可以對(duì)同一個(gè)資源進(jìn)行訪問,但是只有一個(gè)線程能夠成功修改資源。

無鎖的特點(diǎn)就是在循環(huán)內(nèi)進(jìn)行修改操作,線程會(huì)不斷的嘗試修改共享資源,直到能夠成功修改資源并退出,在此過程中沒有出現(xiàn)沖突的發(fā)生,這很像我們?cè)谥拔恼轮薪榻B的 CAS 實(shí)現(xiàn),CAS 的原理和應(yīng)用就是無鎖的實(shí)現(xiàn)。無鎖無法全面代替有鎖,但無鎖在某些場(chǎng)合下的性能是非常高的。
偏向鎖
HotSpot 的作者經(jīng)過研究發(fā)現(xiàn),大多數(shù)情況下,鎖不僅不存在多線程競(jìng)爭(zhēng),還存在鎖由同一線程多次獲得的情況,偏向鎖就是在這種情況下出現(xiàn)的,它的出現(xiàn)是為了解決只有在一個(gè)線程執(zhí)行同步時(shí)提高性能。

可以從對(duì)象頭的分配中看到,偏向鎖要比無鎖多了線程ID 和 epoch,下面我們就來描述一下偏向鎖的獲取過程
偏向鎖獲取過程
- 首先線程訪問同步代碼塊,會(huì)通過檢查對(duì)象頭 Mark Word 的
鎖標(biāo)志位判斷目前鎖的狀態(tài),如果是 01,說明就是無鎖或者偏向鎖,然后再根據(jù)是否偏向鎖的標(biāo)示判斷是無鎖還是偏向鎖,如果是無鎖情況下,執(zhí)行下一步 - 線程使用 CAS 操作來嘗試對(duì)對(duì)象加鎖,如果使用 CAS 替換 ThreadID 成功,就說明是第一次上鎖,那么當(dāng)前線程就會(huì)獲得對(duì)象的偏向鎖,此時(shí)會(huì)在對(duì)象頭的 Mark Word 中記錄當(dāng)前線程 ID 和獲取鎖的時(shí)間 epoch 等信息,然后執(zhí)行同步代碼塊。
全局安全點(diǎn)(Safe Point):全局安全點(diǎn)的理解會(huì)涉及到 C 語言底層的一些知識(shí),這里簡(jiǎn)單理解 SafePoint 是 Java 代碼中的一個(gè)線程可能暫停執(zhí)行的位置。
等到下一次線程在進(jìn)入和退出同步代碼塊時(shí)就不需要進(jìn)行 CAS 操作進(jìn)行加鎖和解鎖,只需要簡(jiǎn)單判斷一下對(duì)象頭的 Mark Word 中是否存儲(chǔ)著指向當(dāng)前線程的線程ID,判斷的標(biāo)志當(dāng)然是根據(jù)鎖的標(biāo)志位來判斷的。如果用流程圖來表示的話就是下面這樣

關(guān)閉偏向鎖
偏向鎖在Java 6 和Java 7 里是默認(rèn)啟用的。由于偏向鎖是為了在只有一個(gè)線程執(zhí)行同步塊時(shí)提高性能,如果你確定應(yīng)用程序里所有的鎖通常情況下處于競(jìng)爭(zhēng)狀態(tài),可以通過JVM參數(shù)關(guān)閉偏向鎖:-XX:-UseBiasedLocking=false,那么程序默認(rèn)會(huì)進(jìn)入輕量級(jí)鎖狀態(tài)。
關(guān)于 epoch
偏向鎖的對(duì)象頭中有一個(gè)被稱為 epoch 的值,它作為偏差有效性的時(shí)間戳。
輕量級(jí)鎖
輕量級(jí)鎖是指當(dāng)前鎖是偏向鎖的時(shí)候,資源被另外的線程所訪問,那么偏向鎖就會(huì)升級(jí)為輕量級(jí)鎖,其他線程會(huì)通過自旋的形式嘗試獲取鎖,不會(huì)阻塞,從而提高性能,下面是詳細(xì)的獲取過程。
輕量級(jí)鎖加鎖過程
- 緊接著上一步,如果 CAS 操作替換 ThreadID 沒有獲取成功,執(zhí)行下一步
- 如果使用 CAS 操作替換 ThreadID 失敗(這時(shí)候就切換到另外一個(gè)線程的角度)說明該資源已被同步訪問過,這時(shí)候就會(huì)執(zhí)行鎖的撤銷操作,撤銷偏向鎖,然后等原持有偏向鎖的線程到達(dá)
全局安全點(diǎn)(SafePoint)時(shí),會(huì)暫停原持有偏向鎖的線程,然后會(huì)檢查原持有偏向鎖的狀態(tài),如果已經(jīng)退出同步,就會(huì)喚醒持有偏向鎖的線程,執(zhí)行下一步 - 檢查對(duì)象頭中的 Mark Word 記錄的是否是當(dāng)前線程 ID,如果是,執(zhí)行同步代碼,如果不是,執(zhí)行偏向鎖獲取流程 的第2步。
如果用流程表示的話就是下面這樣(已經(jīng)包含偏向鎖的獲取)

重量級(jí)鎖
重量級(jí)鎖的獲取流程比較復(fù)雜,小伙伴們做好準(zhǔn)備,其實(shí)多看幾遍也沒那么麻煩,呵呵。
重量級(jí)鎖的獲取流程
1.?接著上面偏向鎖的獲取過程,由偏向鎖升級(jí)為輕量級(jí)鎖,執(zhí)行下一步
2 .?會(huì)在原持有偏向鎖的線程的棧中分配鎖記錄,將對(duì)象頭中的 Mark Word 拷貝到原持有偏向鎖線程的記錄中,然后原持有偏向鎖的線程獲得輕量級(jí)鎖,然后喚醒原持有偏向鎖的線程,從安全點(diǎn)處繼續(xù)執(zhí)行,執(zhí)行完畢后,執(zhí)行下一步,當(dāng)前線程執(zhí)行第4步
3. 執(zhí)行完畢后,開始輕量級(jí)解鎖操作,解鎖需要判斷兩個(gè)條件
????????如果上面兩個(gè)判斷條件都符合的話,就進(jìn)行鎖釋放,如果其中一個(gè)條件不?符合,就會(huì)釋放鎖,并喚起等待的線程,進(jìn)行新一輪的鎖競(jìng)爭(zhēng)。

4. 在當(dāng)前線程的棧中分配鎖記錄,拷貝對(duì)象頭中的 MarkWord 到當(dāng)前線程的鎖記錄中,執(zhí)行 CAS 加鎖操作,會(huì)把對(duì)象頭 Mark Word 中鎖記錄指針指向當(dāng)前線程鎖記錄,如果成功,獲取輕量級(jí)鎖,執(zhí)行同步代碼,然后執(zhí)行第3步,如果不成功,執(zhí)行下一步
5.?當(dāng)前線程沒有使用 CAS 成功獲取鎖,就會(huì)自旋一會(huì)兒,再次嘗試獲取,如果在多次自旋到達(dá)上限后還沒有獲取到鎖,那么輕量級(jí)鎖就會(huì)升級(jí)為 重量級(jí)鎖

如果用流程圖表示是這樣的

鎖的公平性與非公平性
我們知道,在并發(fā)環(huán)境中,多個(gè)線程需要對(duì)同一資源進(jìn)行訪問,同一時(shí)刻只能有一個(gè)線程能夠獲取到鎖并進(jìn)行資源訪問,那么剩下的這些線程怎么辦呢?這就好比食堂排隊(duì)打飯的模型,最先到達(dá)食堂的人擁有最先買飯的權(quán)利,那么剩下的人就需要在第一個(gè)人后面排隊(duì),這是理想的情況,即每個(gè)人都能夠買上飯。那么現(xiàn)實(shí)情況是,在你排隊(duì)的過程中,就有個(gè)別不老實(shí)的人想走捷徑,插隊(duì)打飯,如果插隊(duì)的這個(gè)人后面沒有人制止他這種行為,他就能夠順利買上飯,如果有人制止,他就也得去隊(duì)伍后面排隊(duì)。
對(duì)于正常排隊(duì)的人來說,沒有人插隊(duì),每個(gè)人都在等待排隊(duì)打飯的機(jī)會(huì),那么這種方式對(duì)每個(gè)人來說都是公平的,先來后到嘛。這種鎖也叫做公平鎖。

那么假如插隊(duì)的這個(gè)人成功買上飯并且在買飯的過程不管有沒有人制止他,他的這種行為對(duì)正常排隊(duì)的人來說都是不公平的,這在鎖的世界中也叫做非公平鎖。


那么我們根據(jù)上面的描述可以得出下面的結(jié)論
公平鎖表示線程獲取鎖的順序是按照線程加鎖的順序來分配的,即先來先得的FIFO先進(jìn)先出順序。而非公平鎖就是一種獲取鎖的搶占機(jī)制,是隨機(jī)獲得鎖的,和公平鎖不一樣的就是先來的不一定先得到鎖,這個(gè)方式可能造成某些線程一直拿不到鎖,結(jié)果也就是不公平的了。
鎖公平性的實(shí)現(xiàn)
在 Java 中,我們一般通過 ReetrantLock 來實(shí)現(xiàn)鎖的公平性
我們分別通過兩個(gè)例子來講解一下鎖的公平性和非公平性
鎖的公平性
public class MyFairLock extends Thread{
private ReentrantLock lock = new ReentrantLock(true);
public void fairLock(){
try {
lock.lock();
System.out.println(Thread.currentThread().getName() + "正在持有鎖");
}finally {
System.out.println(Thread.currentThread().getName() + "釋放了鎖");
lock.unlock();
}
}
public static void main(String[] args) {
MyFairLock myFairLock = new MyFairLock();
Runnable runnable = () -> {
System.out.println(Thread.currentThread().getName() + "啟動(dòng)");
myFairLock.fairLock();
};
Thread[] thread = new Thread[10];
for(int i = 0;i < 10;i++){
thread[i] = new Thread(runnable);
}
for(int i = 0;i < 10;i++){
thread[i].start();
}
}
}
我們創(chuàng)建了一個(gè) ReetrantLock,并給構(gòu)造函數(shù)傳了一個(gè) true,我們可以查看 ReetrantLock 的構(gòu)造函數(shù)
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
根據(jù) JavaDoc 的注釋可知,如果是 true 的話,那么就會(huì)創(chuàng)建一個(gè) ReentrantLock 的公平鎖,然后并創(chuàng)建一個(gè) FairSync ,F(xiàn)airSync 其實(shí)是一個(gè) Sync 的內(nèi)部類,它的主要作用是同步對(duì)象以獲取公平鎖。

而 Sync 是 ReentrantLock 中的內(nèi)部類,Sync 繼承 AbstractQueuedSynchronizer 類,AbstractQueuedSynchronizer 就是我們常說的 AQS ,它是 JUC(java.util.concurrent) 中最重要的一個(gè)類,通過它來實(shí)現(xiàn)獨(dú)占鎖和共享鎖。
abstract static class Sync extends AbstractQueuedSynchronizer {...}
也就是說,我們把 fair 參數(shù)設(shè)置為 true 之后,就可以實(shí)現(xiàn)一個(gè)公平鎖了,是這樣嗎?我們回到示例代碼,我們可以執(zhí)行一下這段代碼,它的輸出是順序獲取的(礙于篇幅的原因,這里就暫不貼出了),也就是說我們創(chuàng)建了一個(gè)公平鎖
鎖的非公平性
與公平性相對(duì)的就是非公平性,我們通過設(shè)置 fair 參數(shù)為 true,便實(shí)現(xiàn)了一個(gè)公平鎖,與之相對(duì)的,我們把 fair 參數(shù)設(shè)置為 false,是不是就是非公平鎖了?用事實(shí)證明一下
private ReentrantLock lock = new ReentrantLock(false);
其他代碼不變,我們執(zhí)行一下看看輸出(部分輸出)
Thread-1啟動(dòng)
Thread-4啟動(dòng)
Thread-1正在持有鎖
Thread-1釋放了鎖
Thread-5啟動(dòng)
Thread-6啟動(dòng)
Thread-3啟動(dòng)
Thread-7啟動(dòng)
Thread-2啟動(dòng)
可以看到,線程的啟動(dòng)并沒有按順序獲取,可以看出非公平鎖對(duì)鎖的獲取是亂序的,即有一個(gè)搶占鎖的過程。也就是說,我們把 fair 參數(shù)設(shè)置為 false 便實(shí)現(xiàn)了一個(gè)非公平鎖。
ReentrantLock 基本概述
ReentrantLock 是一把可重入鎖,也是一把互斥鎖,它具有與 synchronized 相同的方法和監(jiān)視器鎖的語義,但是它比 synchronized 有更多可擴(kuò)展的功能。
ReentrantLock 的可重入性是指它可以由上次成功鎖定但還未解鎖的線程擁有。當(dāng)只有一個(gè)線程嘗試加鎖時(shí),該線程調(diào)用 lock() 方法會(huì)立刻返回成功并直接獲取鎖。如果當(dāng)前線程已經(jīng)擁有這把鎖,這個(gè)方法會(huì)立刻返回。可以使用 isHeldByCurrentThread 和 getHoldCount 進(jìn)行檢查。
這個(gè)類的構(gòu)造函數(shù)接受可選擇的 fairness 參數(shù),當(dāng) fairness 設(shè)置為 true 時(shí),在多線程爭(zhēng)奪嘗試加鎖時(shí),鎖傾向于對(duì)等待時(shí)間最長(zhǎng)的線程訪問,這也是公平性的一種體現(xiàn)。否則,鎖不能保證每個(gè)線程的訪問順序,也就是非公平鎖。與使用默認(rèn)設(shè)置的程序相比,使用許多線程訪問的公平鎖的程序可能會(huì)顯示較低的總體吞吐量(即較慢;通常要慢得多)。但是獲取鎖并保證線程不會(huì)饑餓的次數(shù)比較小。無論如何請(qǐng)注意:鎖的公平性不能保證線程調(diào)度的公平性。因此,使用公平鎖的多線程之一可能會(huì)連續(xù)多次獲得它,而其他活動(dòng)線程沒有進(jìn)行且當(dāng)前未持有該鎖。這也是互斥性 的一種體現(xiàn)。
也要注意的 tryLock() 方法不支持公平性。如果鎖是可以獲取的,那么即使其他線程等待,它仍然能夠返回成功。
推薦使用下面的代碼來進(jìn)行加鎖和解鎖
class MyFairLock {
private final ReentrantLock lock = new ReentrantLock();
public void m() {
lock.lock();
try {
// ...
} finally {
lock.unlock()
}
}
}
ReentrantLock 鎖通過同一線程最多支持2147483647個(gè)遞歸鎖。嘗試超過此限制會(huì)導(dǎo)致鎖定方法引發(fā)錯(cuò)誤。
ReentrantLock 如何實(shí)現(xiàn)鎖公平性
我們?cè)谏厦娴暮?jiǎn)述中提到,ReentrantLock 是可以實(shí)現(xiàn)鎖的公平性的,那么原理是什么呢?下面我們通過其源碼來了解一下 ReentrantLock 是如何實(shí)現(xiàn)鎖的公平性的
跟蹤其源碼發(fā)現(xiàn),調(diào)用 Lock.lock() 方法其實(shí)是調(diào)用了 sync 的內(nèi)部的方法
abstract void lock();
而 sync 是最基礎(chǔ)的同步控制 Lock 的類,它有公平鎖和非公平鎖的實(shí)現(xiàn)。它繼承 AbstractQueuedSynchronizer 即 使用 AQS 狀態(tài)代表鎖持有的數(shù)量。
lock 是抽象方法是需要被子類實(shí)現(xiàn)的,而繼承了 AQS 的類主要有

我們可以看到,所有實(shí)現(xiàn)了 AQS 的類都位于 JUC 包下,主要有五類:ReentrantLock、ReentrantReadWriteLock、Semaphore、CountDownLatch 和 ThreadPoolExecutor,其中 ReentrantLock、ReentrantReadWriteLock、Semaphore 都可以實(shí)現(xiàn)公平鎖和非公平鎖。
下面是公平鎖 FairSync 的繼承關(guān)系

非公平鎖的NonFairSync 的繼承關(guān)系

由繼承圖可以看到,兩個(gè)類的繼承關(guān)系都是相同的,我們從源碼發(fā)現(xiàn),公平鎖和非公平鎖的實(shí)現(xiàn)就是下面這段代碼的區(qū)別(下一篇文章我們會(huì)從原理角度分析一下公平鎖和非公平鎖的實(shí)現(xiàn))

通過上圖中的源代碼對(duì)比,我們可以明顯的看出公平鎖與非公平鎖的lock()方法唯一的區(qū)別就在于公平鎖在獲取同步狀態(tài)時(shí)多了一個(gè)限制條件:hasQueuedPredecessors()。
hasQueuedPredecessors() 也是 AQS 中的方法,它主要是用來 查詢是否有任何線程在等待獲取鎖的時(shí)間比當(dāng)前線程長(zhǎng),也就是說每個(gè)等待線程都是在一個(gè)隊(duì)列中,此方法就是判斷隊(duì)列中在當(dāng)前線程獲取鎖時(shí),是否有等待鎖時(shí)間比自己還長(zhǎng)的隊(duì)列,如果當(dāng)前線程之前有排隊(duì)的線程,返回 true,如果當(dāng)前線程位于隊(duì)列的開頭或隊(duì)列為空,返回 false。
綜上,公平鎖就是通過同步隊(duì)列來實(shí)現(xiàn)多個(gè)線程按照申請(qǐng)鎖的順序來獲取鎖,從而實(shí)現(xiàn)公平的特性。非公平鎖加鎖時(shí)不考慮排隊(duì)等待問題,直接嘗試獲取鎖,所以存在后申請(qǐng)卻先獲得鎖的情況。
根據(jù)鎖是否可重入進(jìn)行區(qū)分
可重入鎖
可重入鎖又稱為遞歸鎖,是指在同一個(gè)線程在外層方法獲取鎖的時(shí)候,再進(jìn)入該線程的內(nèi)層方法會(huì)自動(dòng)獲取鎖(前提鎖對(duì)象得是同一個(gè)對(duì)象或者class),不會(huì)因?yàn)橹耙呀?jīng)獲取過還沒釋放而阻塞。Java 中 ReentrantLock 和synchronized 都是可重入鎖,可重入鎖的一個(gè)優(yōu)點(diǎn)是在一定程度上可以避免死鎖。
我們先來看一段代碼來說明一下 synchronized 的可重入性
private synchronized void doSomething(){
System.out.println("doSomething...");
doSomethingElse();
}
private synchronized void doSomethingElse(){
System.out.println("doSomethingElse...");
}
在上面這段代碼中,我們對(duì) doSomething() 和 doSomethingElse() 分別使用了 synchronized 進(jìn)行鎖定,doSomething() 方法中調(diào)用了 doSomethingElse() 方法,因?yàn)?synchronized 是可重入鎖,所以同一個(gè)線程在調(diào)用 doSomething() 方法時(shí),也能夠進(jìn)入 doSomethingElse() 方法中。
不可重入鎖
如果 synchronized 是不可重入鎖的話,那么在調(diào)用 doSomethingElse() 方法的時(shí)候,必須把 doSomething() 的鎖丟掉,實(shí)際上該對(duì)象鎖已被當(dāng)前線程所持有,且無法釋放。所以此時(shí)會(huì)出現(xiàn)死鎖。
也就是說,不可重入鎖會(huì)造成死鎖
多個(gè)線程能夠共享同一把鎖
獨(dú)占鎖和共享鎖
獨(dú)占多和共享鎖一般對(duì)應(yīng) JDK 源碼的 ReentrantLock 和 ReentrantReadWriteLock 源碼來介紹獨(dú)占鎖和共享鎖。
獨(dú)占鎖又叫做排他鎖,是指鎖在同一時(shí)刻只能被一個(gè)線程擁有,其他線程想要訪問資源,就會(huì)被阻塞。JDK 中 synchronized和 JUC 中 Lock 的實(shí)現(xiàn)類就是互斥鎖。
共享鎖指的是鎖能夠被多個(gè)線程所擁有,如果某個(gè)線程對(duì)資源加上共享鎖后,則其他線程只能對(duì)資源再加共享鎖,不能加排它鎖。獲得共享鎖的線程只能讀數(shù)據(jù),不能修改數(shù)據(jù)。

我們看到 ReentrantReadWriteLock 有兩把鎖:ReadLock 和 WriteLock,也就是一個(gè)讀鎖一個(gè)寫鎖,合在一起叫做讀寫鎖。再進(jìn)一步觀察可以發(fā)現(xiàn) ReadLock 和 WriteLock 是靠?jī)?nèi)部類 Sync 實(shí)現(xiàn)的鎖。Sync 是繼承于 AQS 子類的,AQS 是并發(fā)的根本,這種結(jié)構(gòu)在CountDownLatch、ReentrantLock、Semaphore里面也都存在。
在 ReentrantReadWriteLock 里面,讀鎖和寫鎖的鎖主體都是 Sync,但讀鎖和寫鎖的加鎖方式不一樣。讀鎖是共享鎖,寫鎖是獨(dú)享鎖。讀鎖的共享鎖可保證并發(fā)讀非常高效,而讀寫、寫讀、寫寫的過程互斥,因?yàn)樽x鎖和寫鎖是分離的。所以ReentrantReadWriteLock的并發(fā)性相比一般的互斥鎖有了很大提升。
其他優(yōu)質(zhì)并發(fā)文章推薦
1、一句話擼完重量級(jí)鎖、自旋鎖、輕量級(jí)鎖、偏向鎖、悲觀、樂觀鎖等各種鎖
2、線程安全(上)--徹底搞懂volatile關(guān)鍵字
3、線程安全(下)--徹底搞懂synchronized(從偏向鎖到重量級(jí)鎖)
另外,我正在整理一份計(jì)算機(jī)類書單,只為讓大家更加方便找到自己想要的書籍,目前已經(jīng)收集了幾百本了,貢獻(xiàn)給需要的人,目前是整理到了 github 上,方便更新,地址:https://github.com/iamshuaidi/CS-Book(點(diǎn)擊左下角的閱讀原文,可直達(dá))。
截圖了部分目錄如下:

更多算法 + 計(jì)算機(jī)基礎(chǔ)知識(shí)(計(jì)算機(jī)網(wǎng)絡(luò)+ 操作系統(tǒng)+數(shù)據(jù)庫(kù)+Linux等)文章,歡迎更多我的微信公眾號(hào)哦。
