深入理解Java并發(fā)編程

JMM基礎(chǔ)-計(jì)算機(jī)原理

Java內(nèi)存模型即Java Memory Model,簡(jiǎn)稱JMM。JMM定義了Java 虛擬機(jī)(JVM)在計(jì)算機(jī)內(nèi)存(RAM)中的工作方式。JVM是整個(gè)計(jì)算機(jī)虛擬模型,所以JMM是隸屬于JVM的。Java1.5版本對(duì)其進(jìn)行了重構(gòu),現(xiàn)在的Java仍沿用了Java1.5的版本。
JMM遇到的問(wèn)題與現(xiàn)代計(jì)算機(jī)中遇到的問(wèn)題是差不多的

《Jeff Dean在Google全體工程大會(huì)的報(bào)告》
image.png

計(jì)算機(jī)在做一些我們平時(shí)的基本操作時(shí),需要的響應(yīng)時(shí)間是不一樣的

如果從內(nèi)存中讀取1M的int型數(shù)據(jù)由CPU進(jìn)行累加,耗時(shí)要多久?

做個(gè)簡(jiǎn)單的計(jì)算,1M的數(shù)據(jù),Java里int型為32位,4個(gè)字節(jié),共有1024*1024/4 = 262144個(gè)整數(shù) ,則CPU 計(jì)算耗時(shí):262144 0.6 = 157 286 納秒,而我們知道從內(nèi)存讀取1M數(shù)據(jù)需要250000納秒,兩者雖然有差距(當(dāng)然這個(gè)差距并不小,十萬(wàn)納秒的時(shí)間足夠CPU執(zhí)行將近二十萬(wàn)條指令了),但是還在一個(gè)數(shù)量級(jí)上。但是,沒(méi)有任何緩存機(jī)制的情況下,意味著每個(gè)數(shù)都需要從內(nèi)存中讀取,這樣加上CPU讀取一次內(nèi)存需要100納秒,262144個(gè)整數(shù)從內(nèi)存讀取到CPU加上計(jì)算時(shí)間一共需要262144100+250000 = 26 464 400 納秒,這就存在著數(shù)量級(jí)上的差異了

而且現(xiàn)實(shí)情況中絕大多數(shù)的運(yùn)算任務(wù)都不可能只靠處理器“計(jì)算”就能完成,處理器至少要與內(nèi)存交互,如讀取運(yùn)算數(shù)據(jù)、存儲(chǔ)運(yùn)算結(jié)果等,這個(gè)I/O操作是基本上是無(wú)法消除的(無(wú)法僅靠寄存器來(lái)完成所有運(yùn)算任務(wù))。

  • 早期計(jì)算機(jī)中cpu和內(nèi)存的速度是差不多的,但在現(xiàn)代計(jì)算機(jī)中,cpu的指令速度遠(yuǎn)超內(nèi)存的存取速度,由于計(jì)算機(jī)的存儲(chǔ)設(shè)備與處理器的運(yùn)算速度有幾個(gè)數(shù)量級(jí)的差距,所以現(xiàn)代計(jì)算機(jī)系統(tǒng)都不得不加入一層讀寫(xiě)速度盡可能接近處理器運(yùn)算速度的高速緩存(Cache)來(lái)作為內(nèi)存與處理器之間的緩沖
  • 將運(yùn)算需要使用到的數(shù)據(jù)復(fù)制到緩存中,讓運(yùn)算能快速進(jìn)行,當(dāng)運(yùn)算結(jié)束后再?gòu)木彺嫱交貎?nèi)存之中,這樣處理器就無(wú)須等待緩慢的內(nèi)存讀寫(xiě)了。


    image.png

在計(jì)算機(jī)系統(tǒng)中,寄存器劃是L0級(jí)緩存,接著依次是L1,L2,L3(接下來(lái)是內(nèi)存,本地磁盤,遠(yuǎn)程存儲(chǔ))。

越往上的緩存存儲(chǔ)空間越小,速度越快,成本也更高;越往下的存儲(chǔ)空間越大,速度更慢,成本也更低。從上至下,每一層都可以看做是更下一層的緩存,即:L0寄存器是L1一級(jí)緩存的緩存,L1是L2的緩存,依次類推;每一層的數(shù)據(jù)都是來(lái)至它的下一層,所以每一層的數(shù)據(jù)是下一層的數(shù)據(jù)的子集。


image.png

image.png

在現(xiàn)代CPU上,一般來(lái)說(shuō)L0, L1,L2,L3都集成在CPU內(nèi)部,而L1還分為一級(jí)數(shù)據(jù)緩存(Data Cache,D-Cache,L1d)和一級(jí)指令緩存(Instruction Cache,I-Cache,L1i),分別用于存放數(shù)據(jù)和執(zhí)行數(shù)據(jù)的指令解碼。每個(gè)核心擁有獨(dú)立的運(yùn)算處理單元、控制器、寄存器、L1、L2緩存,然后一個(gè)CPU的多個(gè)核心共享最后一層CPU緩存L3

image.png

Java內(nèi)存模型(JMM)

  • 從抽象的角度來(lái)看,JMM定義了線程和主內(nèi)存之間的抽象關(guān)系:線程之間的共享變量存儲(chǔ)在主內(nèi)存(Main Memory)中,每個(gè)線程都有一個(gè)私有的本地內(nèi)存(Local Memory),本地內(nèi)存中存儲(chǔ)了該線程以讀/寫(xiě)共享變量的副本
  • 本地內(nèi)存是JMM的一個(gè)抽象概念,并不真實(shí)存在。它涵蓋了緩存、寫(xiě)緩沖區(qū)、寄存器以及其他的硬件和編譯器優(yōu)化。
image.png
image.png

正確理解 count=count +1 ?

image.png

可見(jiàn)性

可見(jiàn)性是指當(dāng)多個(gè)線程訪問(wèn)同一個(gè)變量時(shí),一個(gè)線程修改了這個(gè)變量的值,其他線程能夠立即看得到修改的值。

  • 由于線程對(duì)變量的所有操作都必須在工作內(nèi)存中進(jìn)行,而不能直接讀寫(xiě)主內(nèi)存中的變量,那么對(duì)于共享變量V,它們首先是在自己的工作內(nèi)存,之后再同步到主內(nèi)存??墒遣⒉粫?huì)及時(shí)的刷到主存中,而是會(huì)有一定時(shí)間差。很明顯,這個(gè)時(shí)候線程 A 對(duì)變量 V 的操作對(duì)于線程 B 而言就不具備可見(jiàn)性了 。要解決共享對(duì)象可見(jiàn)性這個(gè)問(wèn)題,我們可以使用volatile關(guān)鍵字或者是加鎖。

原子性

原子性:即一個(gè)操作或者多個(gè)操作 要么全部執(zhí)行并且執(zhí)行的過(guò)程不會(huì)被任何因素打斷,要么就都不執(zhí)行。

  • 我們都知道CPU資源的分配都是以線程為單位的,并且是分時(shí)調(diào)用,操作系統(tǒng)允許某個(gè)進(jìn)程執(zhí)行一小段時(shí)間,例如 50 毫秒,過(guò)了 50 毫秒操作系統(tǒng)就會(huì)重新選擇一個(gè)進(jìn)程來(lái)執(zhí)行(我們稱為“任務(wù)切換”),這個(gè) 50 毫秒稱為“時(shí)間片”。而任務(wù)的切換大多數(shù)是在時(shí)間片段結(jié)束以后,
  • 那么線程切換為什么會(huì)帶來(lái)bug呢?因?yàn)椴僮飨到y(tǒng)做任務(wù)切換,可以發(fā)生在任何一條CPU 指令執(zhí)行完!注意,是 CPU 指令,CPU 指令,CPU 指令,而不是高級(jí)語(yǔ)言里的一條語(yǔ)句。比如count++,在java里就是一句話,但高級(jí)語(yǔ)言里一條語(yǔ)句往往需要多條 CPU 指令完成。其實(shí)count++包含了三個(gè)CPU指令!

volatile詳解

volatile特性??梢园褜?duì)volatile變量的單個(gè)讀/寫(xiě),看成是使用同一個(gè)鎖對(duì)這些單個(gè)讀/寫(xiě)操作做了同步


image.png

可以看成


image.png

volatile變量自身具有下列特性:

  • 可見(jiàn)性。對(duì)一個(gè)volatile變量的讀,總是能看到(任意線程)對(duì)這個(gè)volatile變量最后的寫(xiě)入。
  • 原子性:對(duì)任意單個(gè)volatile變量的讀/寫(xiě)具有原子性,但類似于volatile++這種復(fù)合操作不具有原子性。

volatile雖然能保證執(zhí)行完及時(shí)把變量刷到主內(nèi)存中,但對(duì)于count++這種非原子性、多指令的情況,由于線程切換,線程A剛把count=0加載到工作內(nèi)存,線程B就可以開(kāi)始工作了,這樣就會(huì)導(dǎo)致線程A和B執(zhí)行完的結(jié)果都是1,都寫(xiě)到主內(nèi)存中,主內(nèi)存的值還是1不是2

volatile的實(shí)現(xiàn)原理

image.png
  • 通過(guò)對(duì)OpenJDK中的unsafe.cpp源碼的分析,會(huì)發(fā)現(xiàn)被volatile關(guān)鍵字修飾的變量會(huì)存在一個(gè)“l(fā)ock:”的前綴。
  • Lock前綴,Lock不是一種內(nèi)存屏障,但是它能完成類似內(nèi)存屏障的功能。Lock會(huì)對(duì)CPU總線和高速緩存加鎖,可以理解為CPU指令級(jí)的一種鎖。
  • 同時(shí)該指令會(huì)將當(dāng)前處理器緩存行的數(shù)據(jù)直接寫(xiě)會(huì)到系統(tǒng)內(nèi)存中,且這個(gè)寫(xiě)回內(nèi)存的操作會(huì)使在其他CPU里緩存了該地址的數(shù)據(jù)無(wú)效。

synchronized的實(shí)現(xiàn)原理

Synchronized在JVM里的實(shí)現(xiàn)都是基于進(jìn)入和退出Monitor對(duì)象來(lái)實(shí)現(xiàn)方法同步和代碼塊同步,雖然具體實(shí)現(xiàn)細(xì)節(jié)不一樣,但是都可以通過(guò)成對(duì)的MonitorEnter和MonitorExit指令來(lái)實(shí)現(xiàn)。

  • 對(duì)同步塊,MonitorEnter指令插入在同步代碼塊的開(kāi)始位置,當(dāng)代碼執(zhí)行到該指令時(shí),將會(huì)嘗試獲取該對(duì)象Monitor的所有權(quán),即嘗試獲得該對(duì)象的鎖,而monitorExit指令則插入在方法結(jié)束處和異常處,JVM保證每個(gè)MonitorEnter必須有對(duì)應(yīng)的MonitorExit。

  • 對(duì)同步方法,從同步方法反編譯的結(jié)果來(lái)看,方法的同步并沒(méi)有通過(guò)指令monitorenter和monitorexit來(lái)實(shí)現(xiàn),相對(duì)于普通方法,其常量池中多了ACC_SYNCHRONIZED標(biāo)示符。

  • JVM就是根據(jù)該標(biāo)示符來(lái)實(shí)現(xiàn)方法的同步的:當(dāng)方法被調(diào)用時(shí),調(diào)用指令將會(huì)檢查方法的 ACC_SYNCHRONIZED 訪問(wèn)標(biāo)志是否被設(shè)置,如果設(shè)置了,執(zhí)行線程將先獲取monitor,獲取成功之后才能執(zhí)行方法體,方法執(zhí)行完后再釋放monitor。在方法執(zhí)行期間,其他任何線程都無(wú)法再獲得同一個(gè)monitor對(duì)象。

synchronized使用的鎖是存放在Java對(duì)象頭里面

image.png

具體位置是對(duì)象頭里面的MarkWord,MarkWord里默認(rèn)數(shù)據(jù)是存儲(chǔ)對(duì)象的HashCode等信息

image.png

但是會(huì)隨著對(duì)象的運(yùn)行改變而發(fā)生變化,不同的鎖狀態(tài)對(duì)應(yīng)著不同的記錄存儲(chǔ)方式
image.png

了解各種鎖

image.png

1. 樂(lè)觀鎖 VS 悲觀鎖

  • 樂(lè)觀鎖與悲觀鎖是一種廣義上的概念,體現(xiàn)了看待線程同步的不同角度。在Java和數(shù)據(jù)庫(kù)中都有此概念對(duì)應(yīng)的實(shí)際應(yīng)用。

  • 先說(shuō)概念。對(duì)于同一個(gè)數(shù)據(jù)的并發(fā)操作,悲觀鎖認(rèn)為自己在使用數(shù)據(jù)的時(shí)候一定有別的線程來(lái)修改數(shù)據(jù),因此在獲取數(shù)據(jù)的時(shí)候會(huì)先加鎖,確保數(shù)據(jù)不會(huì)被別的線程修改。Java中,synchronized關(guān)鍵字和Lock的實(shí)現(xiàn)類都是悲觀鎖。

  • 樂(lè)觀鎖認(rèn)為自己在使用數(shù)據(jù)時(shí)不會(huì)有別的線程修改數(shù)據(jù),所以不會(huì)添加鎖,只是在更新數(shù)據(jù)的時(shí)候去判斷之前有沒(méi)有別的線程更新了這個(gè)數(shù)據(jù)。如果這個(gè)數(shù)據(jù)沒(méi)有被更新,當(dāng)前線程將自己修改的數(shù)據(jù)成功寫(xiě)入。如果數(shù)據(jù)已經(jīng)被其他線程更新,則根據(jù)不同的實(shí)現(xiàn)方式執(zhí)行不同的操作(例如報(bào)錯(cuò)或者自動(dòng)重試)。

  • 樂(lè)觀鎖在Java中是通過(guò)使用無(wú)鎖編程來(lái)實(shí)現(xiàn),最常采用的是CAS算法,Java原子類中的遞增操作就通過(guò)CAS自旋實(shí)現(xiàn)的。**

悲觀鎖適合寫(xiě)操作多的場(chǎng)景,先加鎖可以保證寫(xiě)操作時(shí)數(shù)據(jù)正確。
樂(lè)觀鎖適合讀操作多的場(chǎng)景,不加鎖的特點(diǎn)能夠使其讀操作的性能大幅提升。

CAS全稱 Compare And Swap(比較與交換),是一種無(wú)鎖算法。在不使用鎖(沒(méi)有線程被阻塞)的情況下實(shí)現(xiàn)多線程之間的變量同步。java.util.concurrent包中的原子類就是通過(guò)CAS來(lái)實(shí)現(xiàn)了樂(lè)觀鎖。

  • CAS算法涉及到三個(gè)操作數(shù):
  • 需要讀寫(xiě)的內(nèi)存值 V。
  • 進(jìn)行比較的值 A。
  • 要寫(xiě)入的新值 B。

CAS雖然很高效,但是它也存在三大問(wèn)題

  • ABA問(wèn)題。CAS需要在操作值的時(shí)候檢查內(nèi)存值是否發(fā)生變化,沒(méi)有發(fā)生變化才會(huì)更新內(nèi)存值。但是如果內(nèi)存值原來(lái)是A,后來(lái)變成了B,然后又變成了A,那么CAS進(jìn)行檢查時(shí)會(huì)發(fā)現(xiàn)值沒(méi)有發(fā)生變化,但是實(shí)際上是有變化的。ABA問(wèn)題的解決思路就是在變量前面添加版本號(hào),每次變量更新的時(shí)候都把版本號(hào)加一,這樣變化過(guò)程就從“A-B-A”變成了“1A-2B-3A”。JDK從1.5開(kāi)始提供了AtomicStampedReference類來(lái)解決ABA問(wèn)題,具體操作封裝在compareAndSet()中。compareAndSet()首先檢查當(dāng)前引用和當(dāng)前標(biāo)志與預(yù)期引用和預(yù)期標(biāo)志是否相等,如果都相等,則以原子方式將引用值和標(biāo)志的值設(shè)置為給定的更新值。
  • 循環(huán)時(shí)間長(zhǎng)開(kāi)銷大。CAS操作如果長(zhǎng)時(shí)間不成功,會(huì)導(dǎo)致其一直自旋,給CPU帶來(lái)非常大的開(kāi)銷。
  • 只能保證一個(gè)共享變量的原子操作。對(duì)一個(gè)共享變量執(zhí)行操作時(shí),CAS能夠保證原子操作,但是對(duì)多個(gè)共享變量操作時(shí),CAS是無(wú)法保證操作的原子性的。Java從1.5開(kāi)始JDK提供了AtomicReference類來(lái)保證引用對(duì)象之間的原子性,可以把多個(gè)變量放在一個(gè)對(duì)象里來(lái)進(jìn)行CAS操作。

2. 自旋鎖 VS 適應(yīng)性自旋鎖

  • 阻塞或喚醒一個(gè)Java線程需要操作系統(tǒng)切換CPU狀態(tài)來(lái)完成,這種狀態(tài)轉(zhuǎn)換需要耗費(fèi)處理器時(shí)間。如果同步代碼塊中的內(nèi)容過(guò)于簡(jiǎn)單,狀態(tài)轉(zhuǎn)換消耗的時(shí)間有可能比用戶代碼執(zhí)行的時(shí)間還要長(zhǎng)。

*在許多場(chǎng)景中,同步資源的鎖定時(shí)間很短,為了這一小段時(shí)間去切換線程,線程掛起和恢復(fù)現(xiàn)場(chǎng)的花費(fèi)可能會(huì)讓系統(tǒng)得不償失。如果物理機(jī)器有多個(gè)處理器,能夠讓兩個(gè)或以上的線程同時(shí)并行執(zhí)行,我們就可以讓后面那個(gè)請(qǐng)求鎖的線程不放棄CPU的執(zhí)行時(shí)間,看看持有鎖的線程是否很快就會(huì)釋放鎖。

  • 而為了讓當(dāng)前線程“稍等一下”,我們需讓當(dāng)前線程進(jìn)行自旋,如果在自旋完成后前面鎖定同步資源的線程已經(jīng)釋放了鎖,那么當(dāng)前線程就可以不必阻塞而是直接獲取同步資源,從而避免切換線程的開(kāi)銷。這就是自旋鎖。
image.png

自旋鎖本身是有缺點(diǎn)的,它不能代替阻塞。自旋等待雖然避免了線程切換的開(kāi)銷,但它要占用處理器時(shí)間。如果鎖被占用的時(shí)間很短,自旋等待的效果就會(huì)非常好。反之,如果鎖被占用的時(shí)間很長(zhǎng),那么自旋的線程只會(huì)白浪費(fèi)處理器資源。所以,自旋等待的時(shí)間必須要有一定的限度,如果自旋超過(guò)了限定次數(shù)(默認(rèn)是10次,可以使用-XX:PreBlockSpin來(lái)更改)沒(méi)有成功獲得鎖,就應(yīng)當(dāng)掛起線程。

自旋鎖的實(shí)現(xiàn)原理同樣也是CAS,AtomicInteger中調(diào)用unsafe進(jìn)行自增操作的源碼中的do-while循環(huán)就是一個(gè)自旋操作,如果修改數(shù)值失敗則通過(guò)循環(huán)來(lái)執(zhí)行自旋,直至修改成功。


image.png

3. 無(wú)鎖 VS 偏向鎖 VS 輕量級(jí)鎖 VS 重量級(jí)鎖,這四種鎖是指鎖的狀態(tài),專門針對(duì)synchronized的

兩個(gè)重要的概念:“Java對(duì)象頭”、“Monitor”。

Java對(duì)象頭
synchronized是悲觀鎖,在操作同步資源之前需要給同步資源先加鎖,這把鎖就是存在Java對(duì)象頭里的,而Java對(duì)象頭又是什么呢?
我們以Hotspot虛擬機(jī)為例,Hotspot的對(duì)象頭主要包括兩部分?jǐn)?shù)據(jù):Mark Word(標(biāo)記字段)、Klass Pointer(類型指針)。

  • Mark Word:默認(rèn)存儲(chǔ)對(duì)象的HashCode,分代年齡和鎖標(biāo)志位信息。這些信息都是與對(duì)象自身定義無(wú)關(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ǔ)空間,也就是說(shuō)在運(yùn)行期間Mark Word里存儲(chǔ)的數(shù)據(jù)會(huì)隨著鎖標(biāo)志位的變化而變化。

  • Klass Point:對(duì)象指向它的類元數(shù)據(jù)的指針,虛擬機(jī)通過(guò)這個(gè)指針來(lái)確定這個(gè)對(duì)象是哪個(gè)類的實(shí)例。

Monitor
Monitor可以理解為一個(gè)同步工具或一種同步機(jī)制,通常被描述為一個(gè)對(duì)象。每一個(gè)Java對(duì)象就有一把看不見(jiàn)的鎖,稱為內(nèi)部鎖或者M(jìn)onitor鎖。

synchronized最初實(shí)現(xiàn)同步的方式:阻塞或喚醒一個(gè)Java線程需要操作系統(tǒng)切換CPU狀態(tài)來(lái)完成,這種狀態(tài)轉(zhuǎn)換需要耗費(fèi)處理器時(shí)間。如果同步代碼塊中的內(nèi)容過(guò)于簡(jiǎn)單,狀態(tài)轉(zhuǎn)換消耗的時(shí)間有可能比用戶代碼執(zhí)行的時(shí)間還要長(zhǎng),這就是JDK 6之前synchronized效率低的原因。這種依賴于操作系統(tǒng)Mutex Lock所實(shí)現(xiàn)的鎖我們稱之為“重量級(jí)鎖”,JDK 6中為了減少獲得鎖和釋放鎖帶來(lái)的性能消耗,引入了“偏向鎖”和“輕量級(jí)鎖”
image.png

目前鎖一共有4種狀態(tài),級(jí)別從低到高依次是:無(wú)鎖、偏向鎖、輕量級(jí)鎖和重量級(jí)鎖。鎖狀態(tài)只能升級(jí)不能降級(jí)。

image.png

無(wú)鎖
無(wú)鎖沒(méi)有對(duì)資源進(jìn)行鎖定,所有的線程都能訪問(wèn)并修改同一個(gè)資源,但同時(shí)只有一個(gè)線程能修改成功。

無(wú)鎖的特點(diǎn)就是修改操作在循環(huán)內(nèi)進(jìn)行,線程會(huì)不斷的嘗試修改共享資源。如果沒(méi)有沖突就修改成功并退出,否則就會(huì)繼續(xù)循環(huán)嘗試。如果有多個(gè)線程修改同一個(gè)值,必定會(huì)有一個(gè)線程能修改成功,而其他修改失敗的線程會(huì)不斷重試直到修改成功。CAS原理及應(yīng)用即是無(wú)鎖的實(shí)現(xiàn)。無(wú)鎖無(wú)法全面代替有鎖,但無(wú)鎖在某些場(chǎng)合下的性能是非常高的。

偏向鎖
偏向鎖是指一段同步代碼一直被一個(gè)線程所訪問(wèn),那么該線程會(huì)自動(dòng)獲取鎖,降低獲取鎖的代價(jià)。
在大多數(shù)情況下,鎖總是由同一線程多次獲得,不存在多線程競(jìng)爭(zhēng),所以出現(xiàn)了偏向鎖。其目標(biāo)就是在只有一個(gè)線程執(zhí)行同步代碼塊時(shí)能夠提高性能。
輕量級(jí)鎖
是指當(dāng)鎖是偏向鎖的時(shí)候,被另外的線程所訪問(wèn),偏向鎖就會(huì)升級(jí)為輕量級(jí)鎖,其他線程會(huì)通過(guò)自旋的形式嘗試獲取鎖,不會(huì)阻塞,從而提高性能。

重量級(jí)鎖
升級(jí)為重量級(jí)鎖時(shí),鎖標(biāo)志的狀態(tài)值變?yōu)椤?0”,此時(shí)Mark Word中存儲(chǔ)的是指向重量級(jí)鎖的指針,此時(shí)等待鎖的線程都會(huì)進(jìn)入阻塞狀態(tài)。

image.png

4. 公平鎖 VS 非公平鎖

todo

5.可重入鎖 VS 非可重入鎖

可重入鎖又名遞歸鎖,是指在同一個(gè)線程在外層方法獲取鎖的時(shí)候,再進(jìn)入該線程的內(nèi)層方法會(huì)自動(dòng)獲取鎖(前提鎖對(duì)象得是同一個(gè)對(duì)象或者class),不會(huì)因?yàn)橹耙呀?jīng)獲取過(guò)還沒(méi)釋放而阻塞。Java中ReentrantLock和synchronized都是可重入鎖,可重入鎖的一個(gè)優(yōu)點(diǎn)是可一定程度避免死鎖。

ReentrantLock和synchronized都是重入鎖,重入鎖ReentrantLock以及非可重入鎖NonReentrantLock的源碼來(lái)對(duì)比分析一下為什么非可重入鎖在重復(fù)調(diào)用同步資源時(shí)會(huì)出現(xiàn)死鎖。

ReentrantLock和NonReentrantLock都繼承父類AQS,其父類AQS中維護(hù)了一個(gè)同步狀態(tài)status來(lái)計(jì)數(shù)重入次數(shù),status初始值為0。


  • 當(dāng)線程嘗試獲取鎖時(shí),可重入鎖先嘗試獲取并更新status值,如果status == 0表示沒(méi)有其他線程在執(zhí)行同步代碼,則把status置為1,當(dāng)前線程開(kāi)始執(zhí)行。如果status != 0,則判斷當(dāng)前線程是否是獲取到這個(gè)鎖的線程,如果是的話執(zhí)行status+1,且當(dāng)前線程可以再次獲取鎖。而非可重入鎖是直接去獲取并嘗試更新當(dāng)前status的值,如果status != 0的話會(huì)導(dǎo)致其獲取鎖失敗,當(dāng)前線程阻塞。

  • 釋放鎖時(shí),可重入鎖同樣先獲取當(dāng)前status的值,在當(dāng)前線程是持有鎖的線程的前提下。如果status-1 == 0,則表示當(dāng)前線程所有重復(fù)獲取鎖的操作都已經(jīng)執(zhí)行完畢,然后該線程才會(huì)真正釋放鎖。而非可重入鎖則是在確定當(dāng)前線程是持有鎖的線程之后,直接將status置為0,將鎖釋放。

image.png

6.獨(dú)享鎖 VS 共享鎖

獨(dú)享鎖也叫排他鎖,是指該鎖一次只能被一個(gè)線程所持有。如果線程T對(duì)數(shù)據(jù)A加上排它鎖后,則其他線程不能再對(duì)A加任何類型的鎖。獲得排它鎖的線程即能讀數(shù)據(jù)又能修改數(shù)據(jù)。JDK中的synchronized和JUC中Lock的實(shí)現(xiàn)類就是互斥鎖。

共享鎖是指該鎖可被多個(gè)線程所持有。如果線程T對(duì)數(shù)據(jù)A加上共享鎖后,則其他線程只能對(duì)A再加共享鎖,不能加排它鎖。獲得共享鎖的線程只能讀數(shù)據(jù),不能修改數(shù)據(jù)。

獨(dú)享鎖與共享鎖也是通過(guò)AQS來(lái)實(shí)現(xiàn)的,通過(guò)實(shí)現(xiàn)不同的方法,來(lái)實(shí)現(xiàn)獨(dú)享或者共享。

最后說(shuō)明

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容