#目錄
0.LockSupport & AQS
1.公平鎖與非公平鎖 ReentrantLock & ReadWriteLock
2.可重入鎖(遞歸鎖) ReentrantLock及synchronized
3.自旋鎖
4.讀寫(xiě)鎖 ReadWriteLock & StampedLock
5.閉鎖 CountDownLatch
6.柵欄 CyclicBarrier
7.信號(hào)量 Semaphore
8.Exchanger
9.synchronized
10.synchronized與Lock的區(qū)別
11.死鎖
12.分布式鎖
13.活鎖
14.饑餓
0.LockSupport & AQS
0.1 java.util.concurrent.locks.LockSupport
在Java多線(xiàn)程中属铁,當(dāng)需要阻塞或者喚醒一個(gè)線(xiàn)程時(shí)塑崖,都會(huì)使用LockSupport工具類(lèi)來(lái)完成相應(yīng)的工作蜡塌。
LockSupport定義了一組公共靜態(tài)方法,這些方法提供了最基本的線(xiàn)程阻塞和喚醒功能等缀,
而LockSupport也因此成為了構(gòu)建同步組件的基礎(chǔ)工具蜂绎。
實(shí)際上LockSupport阻塞和喚醒線(xiàn)程的功能是依賴(lài)于sun.misc.Unsafe,
這是一個(gè)很底層的類(lèi)轮听,比如park()方法的功能實(shí)現(xiàn)則是靠unsafe.park()方法。
另外在阻塞線(xiàn)程這一系列方法中岭佳,每個(gè)方法都會(huì)新增一個(gè)帶有Object的阻塞對(duì)象的重載方法血巍。
LockSupport定義了一組:
以park開(kāi)頭的方法用來(lái)阻塞當(dāng)前線(xiàn)程,
以u(píng)npark(Thread)方法來(lái)喚醒一個(gè)被阻塞的線(xiàn)程
阻塞線(xiàn)程方法
方法 | 描述 |
---|---|
void park(): | 阻塞當(dāng)前線(xiàn)程珊随,如果調(diào)用unpark方法或者當(dāng)前線(xiàn)程被中斷述寡,從能從park()方法中返回 |
void park(Object blocker) | 功能同方法1,入?yún)⒃黾右粋€(gè)Object對(duì)象叶洞,用來(lái)記錄導(dǎo)致線(xiàn)程阻塞的阻塞對(duì)象鲫凶,方便進(jìn)行問(wèn)題排查; |
void parkNanos(long nanos) | 阻塞當(dāng)前線(xiàn)程衩辟,最長(zhǎng)不超過(guò)nanos納秒螟炫,增加了超時(shí)返回的特性; |
void parkNanos(Object blocker, long nanos) | 功能同方法3艺晴,入?yún)⒃黾右粋€(gè)Object對(duì)象昼钻,用來(lái)記錄導(dǎo)致線(xiàn)程阻塞的阻塞對(duì)象,方便進(jìn)行問(wèn)題排查封寞; |
void parkUntil(long deadline) | 阻塞當(dāng)前線(xiàn)程然评,知道deadline; |
void parkUntil(Object blocker, long deadline) | 功能同方法5狈究,入?yún)⒃黾右粋€(gè)Object對(duì)象沾瓦,用來(lái)記錄導(dǎo)致線(xiàn)程阻塞的阻塞對(duì)象,方便進(jìn)行問(wèn)題排查谦炒; |
喚醒線(xiàn)程方法
方法 | 描述 |
---|---|
void unpark(Thread thread) | 喚醒處于阻塞狀態(tài)的指定線(xiàn)程 |
0.2 AQS(AbstractQueuedSynchronizer
)
AQS是JDK下提供的一套用于實(shí)現(xiàn)基于FIFO等待隊(duì)列的阻塞鎖和相關(guān)的同步器的一個(gè)同步框架贯莺。
這個(gè)抽象類(lèi)被設(shè)計(jì)為作為一些可用原子int值來(lái)表示狀態(tài)的同步器的基類(lèi)。
AQS管理一個(gè)關(guān)于狀態(tài)信息的單一整數(shù)宁改,該整數(shù)可以表現(xiàn)任何狀態(tài)缕探。
#比如
Semaphore 用它來(lái)表現(xiàn)剩余的許可數(shù),
ReentrantLock 用它來(lái)表現(xiàn)擁有它的線(xiàn)程已經(jīng)請(qǐng)求了多少次鎖还蹲;
FutureTask 用它來(lái)表現(xiàn)任務(wù)的狀態(tài)(尚未開(kāi)始爹耗、運(yùn)行、完成和取消)
使用須知(源碼)
* <h3>Usage</h3>
*
* <p>To use this class as the basis of a synchronizer, redefine the
* following methods, as applicable, by inspecting and/or modifying
* the synchronization state using {@link #getState}, {@link
* #setState} and/or {@link #compareAndSetState}:
*
* <ul>
* <li> {@link #tryAcquire}
* <li> {@link #tryRelease}
* <li> {@link #tryAcquireShared}
* <li> {@link #tryReleaseShared}
* <li> {@link #isHeldExclusively}
* </ul>
*
#以上方法不需要全部實(shí)現(xiàn)谜喊,根據(jù)獲取的鎖的種類(lèi)可以選擇實(shí)現(xiàn)不同的方法:
支持獨(dú)占(排他)獲取鎖的同步器應(yīng)該實(shí)現(xiàn)tryAcquire潭兽、 tryRelease、isHeldExclusively;
支持共享獲取鎖的同步器應(yīng)該實(shí)現(xiàn)tryAcquireShared斗遏、tryReleaseShared山卦、isHeldExclusively。
AQS淺析
AQS的實(shí)現(xiàn)主要在于維護(hù)一個(gè)"volatile int state"(代表共享資源)和
一個(gè)FIFO線(xiàn)程等待隊(duì)列(多線(xiàn)程爭(zhēng)用資源被阻塞時(shí)會(huì)進(jìn)入此隊(duì)列)诵次。
隊(duì)列中的每個(gè)節(jié)點(diǎn)是對(duì)線(xiàn)程的一個(gè)封裝账蓉,包含線(xiàn)程基本信息枚碗,狀態(tài),等待的資源類(lèi)型等铸本。
#state的訪(fǎng)問(wèn)方式有三種:
getState()
setState()
compareAndSetState()
#AQS定義兩種資源共享方式
Exclusive(獨(dú)占肮雨,只有一個(gè)線(xiàn)程能執(zhí)行,如ReentrantLock)
Share(共享箱玷,多個(gè)線(xiàn)程可同時(shí)執(zhí)行怨规,如Semaphore/CountDownLatch)
不同的自定義同步器爭(zhēng)用共享資源的方式也不同。
自定義同步器在實(shí)現(xiàn)時(shí)只需要實(shí)現(xiàn)共享資源state的獲取與釋放方式即可锡足,
至于具體線(xiàn)程等待隊(duì)列的維護(hù)(如獲取資源失敗入隊(duì)/喚醒出隊(duì)等)椅亚,AQS已經(jīng)在頂層實(shí)現(xiàn)好了会喝。
自定義同步器實(shí)現(xiàn)時(shí)主要實(shí)現(xiàn)以下幾種方法:
>> isHeldExclusively():該線(xiàn)程是否正在獨(dú)占資源晰搀。只有用到condition才需要去實(shí)現(xiàn)它。
>> tryAcquire(int):獨(dú)占方式话浇。嘗試獲取資源扩灯,成功則返回true媚赖,失敗則返回false。
>> tryRelease(int):獨(dú)占方式珠插。嘗試釋放資源惧磺,成功則返回true,失敗則返回false捻撑。
>> tryAcquireShared(int):共享方式磨隘。嘗試獲取資源。負(fù)數(shù)表示失敼嘶肌番捂;0表示成功,但沒(méi)有剩余可用資源江解;正數(shù)表示成功设预,且有剩余資源。
>> tryReleaseShared(int):共享方式犁河。嘗試釋放資源鳖枕,如果釋放后允許喚醒后續(xù)等待結(jié)點(diǎn)返回true,否則返回false桨螺。
#以ReentrantLock為例
state初始化為0宾符,表示未鎖定狀態(tài)。
A線(xiàn)程lock()時(shí)灭翔,會(huì)調(diào)用tryAcquire()獨(dú)占該鎖并將state+1魏烫。
此后,其他線(xiàn)程再tryAcquire()時(shí)就會(huì)失敗,直到A線(xiàn)程unlock()到state=0(即釋放鎖)為止则奥,其它線(xiàn)程才有機(jī)會(huì)獲取該鎖。
當(dāng)然狭园,釋放鎖之前读处,A線(xiàn)程自己是可以重復(fù)獲取此鎖的(state會(huì)累加),這就是可重入的概念唱矛。
但要注意罚舱,獲取多少次就要釋放多么次,這樣才能保證state是能回到零態(tài)的绎谦。
#以CountDownLatch以例
任務(wù)分為N個(gè)子線(xiàn)程去執(zhí)行管闷,state也初始化為N(注意N要與線(xiàn)程個(gè)數(shù)一致)。
這N個(gè)子線(xiàn)程是并行執(zhí)行的窃肠,每個(gè)子線(xiàn)程執(zhí)行完后countDown()一次包个,state會(huì)CAS減1。
等到所有子線(xiàn)程都執(zhí)行完后(即state=0)冤留,會(huì)unpark()主調(diào)用線(xiàn)程碧囊,然后主調(diào)用線(xiàn)程就會(huì)從await()函數(shù)返回,繼續(xù)后余動(dòng)作纤怒。
一般來(lái)說(shuō)糯而,自定義同步器要么是獨(dú)占方法,要么是共享方式泊窘,
他們也只需實(shí)現(xiàn)tryAcquire-tryRelease熄驼、tryAcquireShared-tryReleaseShared中的一種即可。
但AQS也支持自定義同步器同時(shí)實(shí)現(xiàn)獨(dú)占和共享兩種方式烘豹,如"ReentrantReadWriteLock"瓜贾。
0.3鎖基本概念
>> 公平鎖/非公平鎖
>> 可重入鎖
>> 獨(dú)享鎖/共享鎖
>> 互斥鎖/讀寫(xiě)鎖
>> 樂(lè)觀鎖/悲觀鎖
>> 分段鎖
>> 鎖的4種狀態(tài):無(wú)鎖/偏向鎖/輕量級(jí)鎖/重量級(jí)鎖
>> 自旋鎖
上面是很多鎖的名詞,這些分類(lèi)并不是全是指鎖的狀態(tài)携悯,有的指鎖的特性阐虚,有的指鎖的設(shè)計(jì)。
#公平鎖/非公平鎖
公平鎖是指多個(gè)線(xiàn)程按照申請(qǐng)鎖的順序來(lái)獲取鎖蚌卤。
非公平鎖是指多個(gè)線(xiàn)程獲取鎖的順序并不是按照申請(qǐng)鎖的順序实束,
有可能后申請(qǐng)的線(xiàn)程比先申請(qǐng)的線(xiàn)程優(yōu)先獲取鎖。
有可能逊彭,會(huì)造成優(yōu)先級(jí)反轉(zhuǎn)或者饑餓現(xiàn)象咸灿。
對(duì)于Java ReentrantLock而言,通過(guò)構(gòu)造函數(shù)指定該鎖是否是公平鎖侮叮,默認(rèn)是非公平鎖避矢。
非公平鎖的優(yōu)點(diǎn)在于吞吐量比公平鎖大。
對(duì)于Synchronized而言,也是一種非公平鎖审胸。
由于其并不像ReentrantLock是通過(guò)AQS的來(lái)實(shí)現(xiàn)線(xiàn)程調(diào)度亥宿,
所以并沒(méi)有任何辦法使其變成公平鎖。
#可重入鎖
可重入鎖又名遞歸鎖砂沛,是指在同一個(gè)線(xiàn)程在外層方法獲取鎖的時(shí)候烫扼,在進(jìn)入內(nèi)層方法會(huì)自動(dòng)獲取鎖。
ReentrantLock, Synchronized都是可重入鎖碍庵。
可重入鎖的一個(gè)好處是可一定程度避免死鎖映企。
#獨(dú)享(排他)鎖/共享鎖
獨(dú)享鎖是指該鎖一次只能被一個(gè)線(xiàn)程所持有。
共享鎖是指該鎖可被多個(gè)線(xiàn)程所持有静浴。
對(duì)于Java ReentrantLock而言堰氓,其是獨(dú)享鎖。
但是對(duì)于Lock的另一個(gè)實(shí)現(xiàn)類(lèi)ReadWriteLock苹享,其讀鎖是共享鎖双絮,其寫(xiě)鎖是獨(dú)享鎖。
讀鎖的共享鎖可保證并發(fā)讀是非常高效的得问,讀寫(xiě)掷邦,寫(xiě)讀 ,寫(xiě)寫(xiě)的過(guò)程是互斥的椭赋。
獨(dú)享鎖與共享鎖也是通過(guò)AQS來(lái)實(shí)現(xiàn)的抚岗,通過(guò)實(shí)現(xiàn)不同的方法,來(lái)實(shí)現(xiàn)獨(dú)享或者共享哪怔。
對(duì)于Synchronized而言宣蔚,當(dāng)然是獨(dú)享鎖。
#互斥鎖/讀寫(xiě)鎖
上面講的獨(dú)享鎖/共享鎖就是一種廣義的說(shuō)法认境,互斥鎖/讀寫(xiě)鎖就是具體的實(shí)現(xiàn)胚委。
互斥鎖在Java中的具體實(shí)現(xiàn)就是ReentrantLock
讀寫(xiě)鎖在Java中的具體實(shí)現(xiàn)就是ReadWriteLock
#樂(lè)觀鎖/悲觀鎖
樂(lè)觀鎖與悲觀鎖不是指具體的什么類(lèi)型的鎖,而是指看待并發(fā)同步的角度叉信。
>> 悲觀鎖 (Synchronized 和 ReentrantLock)
認(rèn)為對(duì)于同一個(gè)數(shù)據(jù)的并發(fā)操作亩冬,一定是會(huì)發(fā)生修改的,哪怕沒(méi)有修改硼身,也會(huì)認(rèn)為修改硅急。
因此對(duì)于同一個(gè)數(shù)據(jù)的并發(fā)操作,悲觀鎖采取加鎖的形式佳遂。
悲觀的認(rèn)為营袜,不加鎖的并發(fā)操作一定會(huì)出問(wèn)題。
>> 樂(lè)觀鎖 (java.util.concurrent.atomic包)
認(rèn)為對(duì)于同一個(gè)數(shù)據(jù)的并發(fā)操作丑罪,是不會(huì)發(fā)生修改的荚板。
在更新數(shù)據(jù)的時(shí)候凤壁,會(huì)采用嘗試更新,不斷重新的方式更新數(shù)據(jù)跪另。
樂(lè)觀的認(rèn)為拧抖,不加鎖的并發(fā)操作是沒(méi)有事情的。
悲觀鎖適合寫(xiě)操作非常多的場(chǎng)景免绿,樂(lè)觀鎖適合讀操作非常多的場(chǎng)景唧席,
不加鎖會(huì)帶來(lái)大量的性能提升。
悲觀鎖在Java中的使用针姿,就是利用各種鎖袱吆。
樂(lè)觀鎖在Java中的使用厌衙,是無(wú)鎖編程距淫,常常采用的是CAS算法,
典型的例子就是原子類(lèi)婶希,通過(guò)CAS自旋實(shí)現(xiàn)原子操作的更新榕暇。
#分段鎖
分段鎖其實(shí)是一種鎖的設(shè)計(jì),并不是具體的一種鎖喻杈,ConcurrentHashMap并發(fā)的實(shí)現(xiàn)就是通過(guò)分段鎖的形式來(lái)實(shí)現(xiàn)高效的并發(fā)操作彤枢。
ConcurrentHashMap中的分段鎖稱(chēng)為Segment,
它即類(lèi)似于HashMap(JDK7與JDK8中HashMap的實(shí)現(xiàn))的結(jié)構(gòu)筒饰,
即內(nèi)部擁有一個(gè)Entry數(shù)組缴啡,數(shù)組中的每個(gè)元素又是一個(gè)鏈表;
同時(shí)又是一個(gè)ReentrantLock(Segment繼承了ReentrantLock)瓷们。
當(dāng)需要put元素的時(shí)候业栅,并不是對(duì)整個(gè)hashmap進(jìn)行加鎖,
而是先通過(guò)hashcode來(lái)知道他要放在那一個(gè)分段中谬晕,然后對(duì)這個(gè)分段進(jìn)行加鎖碘裕,
所以當(dāng)多線(xiàn)程put的時(shí)候,只要不是放在一個(gè)分段中攒钳,就實(shí)現(xiàn)了真正的并行的插入帮孔。
但是,在統(tǒng)計(jì)size的時(shí)候不撑,可就是獲取hashmap全局信息的時(shí)候文兢,就需要獲取所有的分段鎖才能統(tǒng)計(jì)。
分段鎖的設(shè)計(jì)目的是細(xì)化鎖的粒度焕檬,當(dāng)操作不需要更新整個(gè)數(shù)組的時(shí)候禽作,
就僅僅針對(duì)數(shù)組中的一項(xiàng)進(jìn)行加鎖操作。
#無(wú)鎖偏向鎖/輕量級(jí)鎖/重量級(jí)鎖
這4種鎖是指鎖的狀態(tài)揩页,并且是針對(duì)Synchronized旷偿。
在Java 5通過(guò)引入鎖升級(jí)的機(jī)制來(lái)實(shí)現(xiàn)高效Synchronized烹俗。
這4種鎖的狀態(tài)是通過(guò)對(duì)象監(jiān)視器在對(duì)象頭中的字段來(lái)表明的。
>> 無(wú)鎖
默認(rèn)就是無(wú)鎖
>> 偏向鎖
是指一段同步代碼一直被一個(gè)線(xiàn)程所訪(fǎng)問(wèn)萍程,那么該線(xiàn)程會(huì)自動(dòng)獲取鎖幢妄。降低獲取鎖的代價(jià)。
>> 輕量級(jí)鎖
是指當(dāng)鎖是偏向鎖的時(shí)候茫负,被另一個(gè)線(xiàn)程所訪(fǎng)問(wèn)蕉鸳,偏向鎖就會(huì)升級(jí)為輕量級(jí)鎖,
其他線(xiàn)程會(huì)通過(guò)自旋的形式嘗試獲取鎖忍法,不會(huì)阻塞潮尝,提高性能。
>> 重量級(jí)鎖
是指當(dāng)鎖為輕量級(jí)鎖的時(shí)候饿序,另一個(gè)線(xiàn)程雖然是自旋勉失,但自旋不會(huì)一直持續(xù)下去,
當(dāng)自旋一定次數(shù)的時(shí)候原探,還沒(méi)有獲取到鎖乱凿,就會(huì)進(jìn)入阻塞隊(duì)列,該鎖膨脹為重量級(jí)鎖咽弦,后續(xù)該線(xiàn)程的調(diào)度依賴(lài)于操作系統(tǒng)的調(diào)度徒蟆。
重量級(jí)鎖會(huì)讓其他申請(qǐng)的線(xiàn)程進(jìn)入阻塞,性能降低型型。
#自旋鎖
在Java中段审,自旋鎖是指嘗試獲取鎖的線(xiàn)程不會(huì)立即阻塞,而是采用循環(huán)的方式去嘗試獲取鎖闹蒜,
這樣的好處是減少線(xiàn)程上下文切換的消耗寺枉,缺點(diǎn)是循環(huán)會(huì)消耗CPU。
典型的自旋鎖實(shí)現(xiàn)的例子嫂用,可以參考自旋鎖的實(shí)現(xiàn)
#為什么while循環(huán)會(huì)消耗CPU呢型凳?
一個(gè)簡(jiǎn)單的 while 死循環(huán)就可以占滿(mǎn)CPU time,
是因?yàn)檫@是你給計(jì)算機(jī)的指示嘱函,你告訴計(jì)算機(jī)要盡可能多的執(zhí)行這個(gè)程序甘畅。
計(jì)算機(jī)就會(huì)把所有可以給它的時(shí)間都給它,導(dǎo)致沒(méi)有Idle時(shí)間往弓。
然而這個(gè)100%并不是真的100%疏唾,他只是拿走了所有可供使用的時(shí)間,操作系統(tǒng)沒(méi)有傻到讓一個(gè)死循環(huán)把CPU全部拿走函似。
如果這時(shí)候你再寫(xiě)一個(gè)while(1)死循環(huán)槐脏,或者說(shuō)開(kāi)一個(gè)需要CPU的大程序,就會(huì)發(fā)現(xiàn)之前的死循環(huán)占用的不是100%了撇寞。
CPU time 100%不代表他占滿(mǎn)了TDP顿天。
1.公平鎖與非公平鎖
// 公平鎖
Lock fairLock = new ReentrantLock(true);
是指多個(gè)線(xiàn)程按照申請(qǐng)鎖的順序來(lái)獲取鎖, 類(lèi)似排隊(duì)買(mǎi)票, 先到先得
// 非公平鎖 (類(lèi)似于 synchronized )
Lock unFairLock = new ReentrantLock(false);
是指多個(gè)線(xiàn)程獲取鎖的順序并不是按照申請(qǐng)鎖的順序, 有可能后申請(qǐng)鎖的先獲取鎖
在高并發(fā)場(chǎng)景下, 可能導(dǎo)致優(yōu)先級(jí)反轉(zhuǎn)或饑餓現(xiàn)象
可在一定程度上提升性能, 提升吞吐量
2.可重入鎖(遞歸鎖) (避免死鎖) ReentrantLock及synchronized
指的是同一個(gè)線(xiàn)程外層函數(shù)獲得鎖之后, 內(nèi)層遞歸函數(shù)仍然能獲得該鎖的代碼,
即線(xiàn)程可以進(jìn)行任何一個(gè)他已經(jīng)獲得鎖的同步著的代碼塊堂氯。
若一個(gè)程序或子程序可以“在任意時(shí)刻被中斷然后操作系統(tǒng)調(diào)度執(zhí)行另外一段代碼,
這段代碼又調(diào)用了該子程序不會(huì)出錯(cuò)”牌废,則稱(chēng)其為可重入(reentrant或re-entrant)的咽白。
即當(dāng)該子程序正在運(yùn)行時(shí),執(zhí)行線(xiàn)程可以再次進(jìn)入并執(zhí)行它鸟缕,仍然獲得符合設(shè)計(jì)時(shí)預(yù)期的結(jié)果晶框。
與多線(xiàn)程并發(fā)執(zhí)行的線(xiàn)程安全不同,可重入強(qiáng)調(diào)對(duì)單個(gè)線(xiàn)程執(zhí)行時(shí)重新進(jìn)入同一個(gè)子程序仍然是安全的懂从。
通俗來(lái)說(shuō):
當(dāng)線(xiàn)程請(qǐng)求一個(gè)由其它線(xiàn)程持有的對(duì)象鎖時(shí)授段,該線(xiàn)程會(huì)阻塞,
而當(dāng)線(xiàn)程請(qǐng)求由自己持有的對(duì)象鎖時(shí),
如果該鎖是重入鎖,請(qǐng)求就會(huì)成功,否則阻塞。
#ReentrantLock版本
package com.zy;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class LockFairLock {
public static void main(String[] args) {
ReentrantLockDemo reentrantLockDemo = new ReentrantLockDemo();
new Thread(reentrantLockDemo, "t1").start();
new Thread(reentrantLockDemo, "t2").start();
}
private static class ReentrantLockDemo implements Runnable {
private Lock lock = new ReentrantLock();
@Override
public void run() {
sendMsg();
}
private void sendMsg() {
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + "---invoke sendMsg()");
sendEmail();
} finally {
lock.unlock();
}
}
private void sendEmail() {
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + "---invoke sendEmail()");
} finally {
lock.unlock();
}
}
}
}
#synchronized版本
public static void main(String[] args) {
// ReentrantLockDemo reentrantLockDemo = new ReentrantLockDemo();
SynchronizedDemo synchronizedDemo = new SynchronizedDemo();
new Thread(() -> {
synchronizedDemo.sendMsg();
}, "t1").start();
new Thread(() -> {
synchronizedDemo.sendMsg();
}, "t2").start();
}
private static class SynchronizedDemo {
public synchronized void sendMsg() {
System.out.println(Thread.currentThread().getName() + "---invoke sendMsg()");
sendEmail();
}
private synchronized void sendEmail() {
System.out.println(Thread.currentThread().getName() + "---invoke sendEmail()");
}
}
3.自旋鎖
簡(jiǎn)單來(lái)講, 是指嘗試獲取鎖的線(xiàn)程不會(huì)立即阻塞, 而是采用循環(huán)的方式去嘗試獲取鎖,
優(yōu)點(diǎn)是可以減少上下文切換的消耗, 缺點(diǎn)是循環(huán)會(huì)消耗CPU
自旋鎖的原理比較簡(jiǎn)單酝枢,如果持有鎖的線(xiàn)程能在短時(shí)間內(nèi)釋放鎖資源,
那么那些等待競(jìng)爭(zhēng)鎖的線(xiàn)程就不需要做內(nèi)核態(tài)和用戶(hù)態(tài)之間的切換進(jìn)入阻塞狀態(tài)渡冻,
它們只需要等一等(自旋)砾赔,等到持有鎖的線(xiàn)程釋放鎖之后即可獲取,這樣就避免了用戶(hù)進(jìn)程和內(nèi)核切換的消耗么翰。
因?yàn)樽孕i避免了操作系統(tǒng)進(jìn)程調(diào)度和線(xiàn)程切換牺汤,所以自旋鎖通常適用在時(shí)間比較短的情況下。
由于這個(gè)原因浩嫌,操作系統(tǒng)的內(nèi)核經(jīng)常使用自旋鎖檐迟。
但是,如果長(zhǎng)時(shí)間上鎖的話(huà)码耐,自旋鎖會(huì)非常耗費(fèi)性能追迟,它阻止了其他線(xiàn)程的運(yùn)行和調(diào)度。
線(xiàn)程持有鎖的時(shí)間越長(zhǎng)骚腥,則持有該鎖的線(xiàn)程將被 OS(Operating System) 調(diào)度程序中斷的風(fēng)險(xiǎn)越大敦间。
如果發(fā)生中斷情況,那么其他線(xiàn)程將保持旋轉(zhuǎn)狀態(tài)(反復(fù)嘗試獲取鎖)束铭,
而持有該鎖的線(xiàn)程并不打算釋放鎖廓块,這樣導(dǎo)致的是結(jié)果是無(wú)限期推遲,
直到持有鎖的線(xiàn)程可以完成并釋放它為止契沫。
解決上面這種情況一個(gè)很好的方式是給自旋鎖設(shè)定一個(gè)自旋時(shí)間带猴,等時(shí)間一到立即釋放自旋鎖。
自旋鎖的目的是占著CPU資源不進(jìn)行釋放懈万,等到獲取鎖立即進(jìn)行處理拴清。
但是如何去選擇自旋時(shí)間呢靶病?
如果自旋執(zhí)行時(shí)間太長(zhǎng),會(huì)有大量的線(xiàn)程處于自旋狀態(tài)占用 CPU 資源口予,進(jìn)而會(huì)影響整體系統(tǒng)的性能嫡秕。
因此自旋的周期選的額外重要!
JDK在1.6 引入了適應(yīng)性自旋鎖苹威,適應(yīng)性自旋鎖意味著自旋時(shí)間不是固定的了昆咽,
而是由前一次在同一個(gè)鎖上的自旋時(shí)間以及鎖擁有的狀態(tài)來(lái)決定,
基本認(rèn)為一個(gè)線(xiàn)程上下文切換的時(shí)間是最佳的一個(gè)時(shí)間牙甫。
自旋鎖的優(yōu)缺點(diǎn)
#優(yōu)點(diǎn)
自旋鎖盡可能的減少線(xiàn)程的阻塞掷酗,這對(duì)于鎖的競(jìng)爭(zhēng)不激烈,
且占用鎖時(shí)間非常短的代碼塊來(lái)說(shuō)性能能大幅度的提升窟哺,
因?yàn)樽孕南臅?huì)小于線(xiàn)程阻塞掛起再喚醒的操作的消耗泻轰,
這些操作會(huì)導(dǎo)致線(xiàn)程發(fā)生兩次上下文切換!
#缺點(diǎn)
但是如果鎖的競(jìng)爭(zhēng)激烈且轨,或者持有鎖的線(xiàn)程需要長(zhǎng)時(shí)間占用鎖執(zhí)行同步塊浮声,
這時(shí)候就不適合使用自旋鎖了,因?yàn)樽孕i在獲取鎖前一直都是占用 cpu 做無(wú)用功旋奢,
占著 XX 不 XX泳挥,同時(shí)有大量線(xiàn)程在競(jìng)爭(zhēng)一個(gè)鎖,會(huì)導(dǎo)致獲取鎖的時(shí)間很長(zhǎng)至朗,
線(xiàn)程自旋的消耗大于線(xiàn)程阻塞掛起操作的消耗屉符,
其它需要 cpu 的線(xiàn)程又不能獲取到 cpu,造成 cpu 的浪費(fèi)锹引。
所以這種情況下我們要關(guān)閉自旋鎖矗钟。
排隊(duì)自旋鎖
// java.util.concurrent.atomic 包下的實(shí)現(xiàn)類(lèi)
這種簡(jiǎn)單的自旋鎖有一個(gè)問(wèn)題:無(wú)法保證多線(xiàn)程競(jìng)爭(zhēng)的公平性。
當(dāng)多個(gè)線(xiàn)程想要獲取鎖時(shí)嫌变,可能會(huì)造成某些線(xiàn)程一直都未獲取到鎖造成線(xiàn)程饑餓吨艇。
// 解決方案
通常我們會(huì)采取排隊(duì)的方式解決這樣的問(wèn)題,類(lèi)似地腾啥,我們把這種鎖叫排隊(duì)自旋鎖(QueuedSpinlock)东涡。
就像我們下課后蜂擁的跑向食堂,下班后蜂擁地?cái)D向地鐵碑宴。
主要有: TicketLock软啼,MCSLock,CLHLock延柠。
package com.zy.tools.undefined.concurrent.lock.spinlock;
import java.util.concurrent.atomic.AtomicReference;
public class SpinLock {
// AtomicReference祸挪,CAS,compareAndSet保證了操作的原子性
private AtomicReference<Thread> owner = new AtomicReference<>();
public void lock() {
// 如果鎖未被占用贞间,則設(shè)置當(dāng)前線(xiàn)程為鎖的擁有者贿条,設(shè)置成功返回true雹仿,否則返回false
// null為期望值,currentThread為要設(shè)置的值整以,如果當(dāng)前內(nèi)存值和期望值null相等胧辽,替換為currentThread
while (!owner.compareAndSet(null, Thread.currentThread())) {
}
}
public void unlock() {
// 只有鎖的擁有者才能釋放鎖,只有上鎖的線(xiàn)程獲取到的currentThread公黑,才能和內(nèi)存中的currentThread相等
owner.compareAndSet(Thread.currentThread(), null);
}
}
3.1 TicketLock
TicketLock 是一種同步機(jī)制或鎖定算法邑商,它是一種自旋鎖,它使用ticket 來(lái)控制線(xiàn)程執(zhí)行順序凡蚜。
就像票據(jù)隊(duì)列管理系統(tǒng)一樣人断。
面包店或者服務(wù)機(jī)構(gòu)(例如銀行)都會(huì)使用這種方式來(lái)為每個(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 類(lèi)型的數(shù)值,開(kāi)始都是0咨堤,
第一個(gè)值是隊(duì)列ticket(隊(duì)列票據(jù))菇篡, 第二個(gè)值是 出隊(duì)(票據(jù))。
隊(duì)列票據(jù)是線(xiàn)程在隊(duì)列中的位置一喘,而出隊(duì)票據(jù)是現(xiàn)在持有鎖的票證的隊(duì)列位置驱还。
簡(jiǎn)單來(lái)說(shuō),就是隊(duì)列票據(jù)是你取票號(hào)的位置凸克,出隊(duì)票據(jù)是你距離叫號(hào)的位置议蟆。
當(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)碼保持一致奄妨,如果一致的話(huà),
那么就輪到你去辦業(yè)務(wù)苹祟,否則只能繼續(xù)等待砸抛。
上面這個(gè)流程的關(guān)鍵點(diǎn)在于,每個(gè)辦業(yè)務(wù)的人在辦完業(yè)務(wù)之后树枫,
他必須丟棄自己的號(hào)碼锰悼,叫號(hào)機(jī)才能繼續(xù)叫到下面的人,
如果這個(gè)人沒(méi)有丟棄這個(gè)號(hào)碼团赏,那么其他人只能繼續(xù)等待箕般。
3.2 CLHLock
3.2.1 前置知識(shí)
#SMP(Symmetric Multi-Processor)
即對(duì)稱(chēng)多處理器結(jié)構(gòu),指服務(wù)器中多個(gè)CPU對(duì)稱(chēng)工作舔清,每個(gè)CPU訪(fǎng)問(wèn)內(nèi)存地址所需時(shí)間相同丝里。
其主要特征是共享,包含對(duì)CPU体谒,內(nèi)存杯聚,I/O等進(jìn)行共享。
SMP的優(yōu)點(diǎn)是能夠保證內(nèi)存一致性抒痒。
SMP的缺點(diǎn)是這些共享的資源很可能成為性能瓶頸幌绍,隨著CPU數(shù)量的增加,
每個(gè)CPU都要訪(fǎng)問(wèn)相同的內(nèi)存資源故响,可能導(dǎo)致內(nèi)存訪(fǎng)問(wèn)沖突傀广,可能會(huì)導(dǎo)致CPU資源的浪費(fèi)。
常用的PC機(jī)就屬于這種彩届。
#NUMA(Non-Uniform Memory Access)
非一致存儲(chǔ)訪(fǎng)問(wèn)伪冰,將CPU分為CPU模塊,每個(gè)CPU模塊由多個(gè)CPU組成樟蠕,
并且具有獨(dú)立的本地內(nèi)存贮聂、I/O槽口等,模塊之間可以通過(guò)互聯(lián)模塊相互訪(fǎng)問(wèn)寨辩,
訪(fǎng)問(wèn)本地內(nèi)存的速度將遠(yuǎn)遠(yuǎn)高于訪(fǎng)問(wèn)遠(yuǎn)地內(nèi)存(系統(tǒng)內(nèi)其它節(jié)點(diǎn)的內(nèi)存)的速度吓懈,
這也是非一致存儲(chǔ)訪(fǎng)問(wèn)NUMA的由來(lái)。
優(yōu)點(diǎn)是可以較好地解決原來(lái)SMP系統(tǒng)的擴(kuò)展問(wèn)題靡狞,
缺點(diǎn)是由于訪(fǎng)問(wèn)遠(yuǎn)地內(nèi)存的延時(shí)遠(yuǎn)遠(yuǎn)超過(guò)本地內(nèi)存耻警,因此當(dāng)CPU數(shù)量增加時(shí),系統(tǒng)性能無(wú)法線(xiàn)性增加。
#CLH
>> CLH隊(duì)列鎖的優(yōu)點(diǎn)
空間復(fù)雜度低榕栏。
如果有n個(gè)線(xiàn)程畔勤,L個(gè)鎖,每個(gè)線(xiàn)程每次只獲取一個(gè)鎖扒磁,
那么需要的存儲(chǔ)空間是O(L+n)庆揪,n個(gè)線(xiàn)程有n個(gè)myNode,L個(gè)鎖有L個(gè)tail妨托,
CLH的一種變體被應(yīng)用在了JAVA并發(fā)框架AQS中缸榛。
>> CLH隊(duì)列鎖的缺點(diǎn)
在NUMA系統(tǒng)結(jié)構(gòu)下性能很差,在這種系統(tǒng)結(jié)構(gòu)下兰伤,每個(gè)線(xiàn)程有自己的內(nèi)存内颗,
如果前趨結(jié)點(diǎn)的內(nèi)存位置比較遠(yuǎn),自旋判斷前趨結(jié)點(diǎn)的locked域敦腔,
性能將大打折扣均澳,但是在SMP系統(tǒng)結(jié)構(gòu)下該法還是非常有效的。
一種解決NUMA系統(tǒng)結(jié)構(gòu)的思路是MCS隊(duì)列鎖符衔。
3.2.2 CLH Lock
package com.zy.tools.undefined.concurrent.lock.spinlock;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
/**
* CLH鎖是自旋鎖的一種找前,AQS源代碼中使用了CLH鎖的一個(gè)變種
* CLH Lock是獨(dú)占式鎖的一種,并且是不可重入的鎖
*
* CLH的算法定義
* CLH lock is Craig, Landin, and Hagersten (CLH) locks,
* CLH lock is a spin lock, can ensure no hunger, provide fairness first come first service.
* The CLH lock is a scalable, high performance, fairness and spin lock based on the list,
* the application thread spin only on a local variable,
* it constantly polling the precursor state, if it is found that the pre release lock end spin.
*
*/
public class ClhSpinLock implements Lock {
private final ThreadLocal<QNode> prev;
private final ThreadLocal<QNode> node;
// step1.初始狀態(tài) tail指向一個(gè)node(head)節(jié)點(diǎn)
private final AtomicReference<QNode> tail = new AtomicReference<>(new QNode());
public ClhSpinLock() {
this.node = ThreadLocal.withInitial(QNode::new);
this.prev = ThreadLocal.withInitial(() -> null);
}
/**
* 1.初始狀態(tài) tail 指向一個(gè)node(head)節(jié)點(diǎn)
* +------+
* | head | <---- tail
* +------+
*
* 2.lock-thread 加入等待隊(duì)列: tail指向新的Node判族,同時(shí)Prev指向tail之前指向的節(jié)點(diǎn)
* +----------+
* | Thread-A |
* | := Node | <---- tail
* | := Prev | -----> +------+
* +----------+ | head |
* +------+
*
* +----------+ +----------+
* | Thread-B | | Thread-A |
* tail ----> | := Node | --> | := Node |
* | := Prev | ----| | := Prev | -----> +------+
* +----------+ +----------+ | head |
* +------+
* 3.尋找當(dāng)前node的prev-node然后開(kāi)始自旋
*
*/
@Override
public void lock() {
final QNode qNode = this.node.get();
qNode.locked = true;
// step2.thread 加入等待隊(duì)列: tail指向新的Node躺盛,同時(shí) prev 指向 tail 之前指向的節(jié)點(diǎn)
QNode pred = this.tail.getAndSet(qNode);
this.prev.set(pred);
// step3.自旋: 尋找當(dāng)前線(xiàn)程對(duì)應(yīng)的node的前驅(qū)node然后開(kāi)始自旋前驅(qū)node的status判斷是否可以獲取lock
while (pred.locked);
}
@Override
public void unlock() {
// 獲取當(dāng)前線(xiàn)程的node,設(shè)置lock status形帮,將當(dāng)前node指向前驅(qū)node(這樣操作tail指向的就是前驅(qū)node等同于出隊(duì)操作).
QNode qNode = this.node.get();
qNode.locked = false;
this.node.set(this.prev.get());
}
@Override
public boolean tryLock() {
return false;
}
@Override
public boolean tryLock(long time, TimeUnit unit) {
return false;
}
@Override
public void lockInterruptibly() throws InterruptedException {
}
@Override
public Condition newCondition() {
return null;
}
private static class QNode {
public volatile boolean locked;
}
}
3.2.3 MCS Lock
4.讀寫(xiě)鎖ReadWriteLock(樂(lè)觀鎖) (共享鎖, 及獨(dú)占鎖)
? ReadWriteLock 維護(hù)了一對(duì)相關(guān)的鎖槽惫,
一個(gè)用于只讀操作,另一個(gè)用于寫(xiě)入操作辩撑。
只要沒(méi)有 writer界斜,讀取鎖可以由多個(gè) reader 線(xiàn)程同時(shí)保持。
寫(xiě)入鎖是獨(dú)占的槐臀。
? ReadWriteLock
讀取操作通常不會(huì)改變共享資源锄蹂,
但執(zhí)行寫(xiě)入操作時(shí),必須獨(dú)占方式來(lái)獲取鎖水慨。
對(duì)于讀取操作占多數(shù)的數(shù)據(jù)結(jié)構(gòu)。
ReadWriteLock 能提供比獨(dú)占鎖更高的并發(fā)性敬扛。
而對(duì)于只讀的數(shù)據(jù)結(jié)構(gòu)晰洒,其中包含的不變性可以完全不需要考慮加鎖操作。
package com.zy.lock;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class ReadWriteLockDemo {
private volatile Map<String, Object> map = new HashMap<>();
private ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
public void put(String key, Object value) {
readWriteLock.writeLock().lock();
try {
System.out.println(Thread.currentThread().getName() + "線(xiàn)程正在寫(xiě)入key--" + key);
try {
TimeUnit.MILLISECONDS.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
map.put(key, value);
System.out.println(Thread.currentThread().getName() + "線(xiàn)程寫(xiě)入key--" + key + "完成");
} finally {
readWriteLock.writeLock().unlock();
}
}
public void get(String key) {
readWriteLock.readLock().lock();
try {
System.out.println(Thread.currentThread().getName() + "線(xiàn)程正在讀取key--" + key);
try {
TimeUnit.MILLISECONDS.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "線(xiàn)程讀取key--" + key + "完成, value是" + map.get(key));
} finally {
readWriteLock.readLock().unlock();
}
}
public static void main(String[] args) {
ReadWriteLockDemo demo = new ReadWriteLockDemo();
for (int i = 0; i < 5; i ++) {
int finalI = i;
new Thread(() -> {
demo.put(String.valueOf(finalI), UUID.randomUUID().toString().substring(0, 3));
demo.get(String.valueOf(finalI));
}).start();
}
}
}
5.CountDownLatch(閉鎖-->減法計(jì)數(shù))
CountDownLatch是通過(guò)AbstractQueuedSynchronizer來(lái)實(shí)現(xiàn)的啥箭,
構(gòu)造器有一個(gè)計(jì)數(shù)器, 計(jì)數(shù)器的初始值為線(xiàn)程的數(shù)量谍珊。
每當(dāng)一個(gè)線(xiàn)程完成了自己的任務(wù)后,計(jì)數(shù)器的值就會(huì)減1急侥。
當(dāng)計(jì)數(shù)器值到達(dá)0時(shí)砌滞,它表示所有的線(xiàn)程已經(jīng)完成了任務(wù)侮邀,
然后在閉鎖上等待的線(xiàn)程就可以恢復(fù)執(zhí)行任務(wù)。
構(gòu)造器中的計(jì)數(shù)值(count)實(shí)際上就是閉鎖需要等待的線(xiàn)程數(shù)量贝润。
這個(gè)值只能被設(shè)置一次绊茧,而且CountDownLatch沒(méi)有提供任何機(jī)制去重新設(shè)置這個(gè)計(jì)數(shù)值。
#舉例
期末考試要開(kāi)始了打掘,監(jiān)考老師發(fā)下去試卷华畏,然后坐在講臺(tái)旁邊玩著手機(jī)等待著學(xué)生答題,有的學(xué)生提前交了試卷尊蚁,并約起打球了亡笑,等到最后一個(gè)學(xué)生交卷了,老師開(kāi)始整理試卷横朋,貼封條仑乌,下班,陪老婆孩子去了琴锭。
應(yīng)用場(chǎng)景
(1)開(kāi)啟多個(gè)線(xiàn)程分塊下載一個(gè)大文件绝骚,
每個(gè)線(xiàn)程只下載固定的一截,最后由另外一個(gè)線(xiàn)程來(lái)拼接所有的分段祠够。
(2)應(yīng)用程序的主線(xiàn)程希望在負(fù)責(zé)啟動(dòng)框架服務(wù)的線(xiàn)程已經(jīng)啟動(dòng)所有的框架服務(wù)之后再執(zhí)行压汪。
(3)確保一個(gè)計(jì)算不會(huì)執(zhí)行,直到所需要的資源被初始化古瓤。
package com.zy.concurrent;
import java.util.concurrent.CountDownLatch;
public class CountDownLatchDemo {
/**
* 遞減的計(jì)數(shù)器, 若未能到達(dá)預(yù)定值, 則一直阻塞
*
* @param args
* @throws InterruptedException
*/
public static void main(String[] args) throws InterruptedException {
// 可以通過(guò)修改構(gòu)造參數(shù)中的 count 的值, 來(lái)觀察控制臺(tái)打印的結(jié)果的情況
CountDownLatch latch = new CountDownLatch(5);
for (int i = 0; i < 5; i++) {
new Thread(() -> {
latch.countDown();
System.out.println(Thread.currentThread().getName() + "-->is running");
}, "thread" + i).start();
}
latch.await();
System.out.println("main thread ---");
}
}
6.CyclicBarrier(柵欄-->加法計(jì)數(shù))
柵欄類(lèi)似于閉鎖止剖,它能阻塞一組線(xiàn)程直到某個(gè)事件的發(fā)生。
柵欄與閉鎖的關(guān)鍵區(qū)別在于落君,所有的線(xiàn)程必須同時(shí)到達(dá)柵欄位置穿香,才能繼續(xù)執(zhí)行铁材。
閉鎖用于等待事件猾普,而柵欄用于等待其他線(xiàn)程。
CyclicBarrier可以使一定數(shù)量的線(xiàn)程反復(fù)地在柵欄位置處匯集瞄桨。
當(dāng)線(xiàn)程到達(dá)柵欄位置時(shí)將調(diào)用await方法纹冤,這個(gè)方法將阻塞直到所有線(xiàn)程都到達(dá)柵欄位置洒宝。
如果所有線(xiàn)程都到達(dá)柵欄位置,那么柵欄將打開(kāi)萌京,此時(shí)所有的線(xiàn)程都將被釋放雁歌,而柵欄將被重置以便下次使用。
#舉例
長(zhǎng)途汽車(chē)站提供長(zhǎng)途客運(yùn)服務(wù)知残。
當(dāng)?shù)却?chē)的乘客到達(dá)20人時(shí)靠瞎,汽車(chē)站就會(huì)發(fā)出一輛長(zhǎng)途汽車(chē),讓這20個(gè)乘客上車(chē)走人。
否則(如只有15個(gè)人時(shí)), 這些人要一直等到等到下次等待的乘客又到達(dá)20人時(shí)乏盐,才會(huì)發(fā)下一輛長(zhǎng)途汽車(chē)佳窑。
與CountDownLatch的對(duì)比
CountDownLatch的計(jì)數(shù)器只能使用一次,
CyclicBarrier的計(jì)數(shù)器可以使用reset()方法重置父能,可以使用多次神凑,
所以CyclicBarrier能夠處理更為復(fù)雜的場(chǎng)景
CyclicBarrier還提供了一些其他有用的方法,
比如getNumberWaiting()方法可以獲得CyclicBarrier阻塞的線(xiàn)程數(shù)量法竞,
isBroken()方法用來(lái)了解阻塞的線(xiàn)程是否被中斷耙厚;
CountDownLatch一般用于某個(gè)線(xiàn)程A(如主線(xiàn)程)等待若干個(gè)其他線(xiàn)程執(zhí)行完任務(wù)之后,它才執(zhí)行岔霸;
CyclicBarrier一般用于一組線(xiàn)程互相等待至某個(gè)狀態(tài)薛躬,然后這一組線(xiàn)程再同時(shí)執(zhí)行;
應(yīng)用場(chǎng)景
CyclicBarrier常用于多線(xiàn)程分組計(jì)算呆细。
package com.zy.concurrent;
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
public class CyclicBarrierDemo {
/**
* 用于多組資源的分別計(jì)算, 之后整合
* 只有當(dāng)任務(wù)數(shù)是構(gòu)造參數(shù)的整數(shù)倍時(shí), 所有任務(wù)才可能全部執(zhí)行
* 否則, 只能執(zhí)行構(gòu)造參數(shù)值的最大整數(shù)倍個(gè)任務(wù), 其余的任務(wù)一直等待 ...
* @param args
*/
public static void main(String[] args) {
// 可以通過(guò)修改構(gòu)造參數(shù)中的 count 的值來(lái)觀察控制臺(tái)結(jié)果打印情況
CyclicBarrier cyclicBarrier = new CyclicBarrier(2);
for (int i = 0; i < 4; i ++) {
new Thread(() -> {
try {
cyclicBarrier.await();
System.out.println(Thread.currentThread().getName() + " is running");
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
}, "thread" + i).start();
}
System.out.println("main thread....");
}
}
7.Semaphore(信號(hào)量, 用于共享資源互斥or并發(fā)線(xiàn)程數(shù)控制)
#應(yīng)用場(chǎng)景:
有一個(gè)停車(chē)場(chǎng)只有5個(gè)車(chē)位型宝,現(xiàn)在有100輛車(chē)要去搶這個(gè)5個(gè)車(chē)位,
理想情況下最多只有五輛車(chē)同時(shí)可以搶到車(chē)位絮爷,
那么沒(méi)有搶到車(chē)位的車(chē)只能等到趴酣,其他的車(chē)讓出車(chē)位,才有機(jī)會(huì)去使用該車(chē)位坑夯。
Semaphore可以用于做流量控制岖寞,特別公用資源有限的應(yīng)用場(chǎng)景,比如數(shù)據(jù)庫(kù)連接柜蜈。
假如有一個(gè)需求仗谆,要讀取幾萬(wàn)個(gè)文件的數(shù)據(jù),因?yàn)槎际荌O密集型任務(wù)淑履,
我們可以啟動(dòng)幾十個(gè)線(xiàn)程并發(fā)的讀取隶垮,但是如果讀到內(nèi)存后,還需要存儲(chǔ)到數(shù)據(jù)庫(kù)中秘噪,
而數(shù)據(jù)庫(kù)的連接數(shù)只有10個(gè)狸吞,這時(shí)我們必須控制只有十個(gè)線(xiàn)程同時(shí)獲取數(shù)據(jù)庫(kù)連接保存數(shù)據(jù),
否則會(huì)報(bào)錯(cuò)無(wú)法獲取數(shù)據(jù)庫(kù)連接指煎。
這個(gè)時(shí)候蹋偏,我們就可以使用Semaphore來(lái)做流控。
package com.zy.concurrent;
import java.util.concurrent.Semaphore;
public class SemaphoreDemo {
/**
* 用于作為線(xiàn)程的信號(hào)量或者是計(jì)數(shù)器
* 如: 停車(chē)場(chǎng)有3個(gè)車(chē)位, 但是來(lái)了8輛車(chē), 則先進(jìn)3輛, 待有車(chē)離開(kāi)后, 再進(jìn)后幾輛
* @param args
*/
public static void main(String[] args) {
Semaphore semaphore = new Semaphore(3, true); // 模擬3個(gè)停車(chē)位
for (int i = 0; i < 8; i ++) { // 模擬8輛車(chē)
new Thread(() -> {
try {
semaphore.acquire();
System.out.println(Thread.currentThread().getName() + " is running");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 可以通過(guò)注釋本行代碼, 來(lái)觀察控制臺(tái)結(jié)果打印情況
semaphore.release();
}
}, "thread " + i).start();
}
System.out.println("main thread ...");
}
}
8.Exchanger
#用法:
此類(lèi)提供對(duì)外的操作是同步的贯要;
用于成對(duì)出現(xiàn)的線(xiàn)程之間交換數(shù)據(jù)暖侨;
可以視作雙向的同步隊(duì)列;
可應(yīng)用于基因算法崇渗、流水線(xiàn)設(shè)計(jì)等場(chǎng)景。
Exchanger類(lèi)僅可用作兩個(gè)線(xiàn)程的信息交換,
當(dāng)超過(guò)兩個(gè)線(xiàn)程調(diào)用同一個(gè)exchanger對(duì)象時(shí)宅广,得到的結(jié)果是隨機(jī)的葫掉,
exchanger對(duì)象僅關(guān)心其包含的兩個(gè)“格子”是否已被填充數(shù)據(jù),
當(dāng)兩個(gè)格子都填充數(shù)據(jù)完成時(shí)跟狱,該對(duì)象就認(rèn)為線(xiàn)程之間已經(jīng)配對(duì)成功俭厚,然后開(kāi)始執(zhí)行數(shù)據(jù)交換操作。
package com.zy.tools.undefined.concurrent.lock;
import java.util.concurrent.Exchanger;
public class ExchangerDemo {
public static void main(String[] args) {
Exchanger<Integer> exchanger = new Exchanger<>();
new Thread(() -> {
try {
int a = 3;
a = exchanger.exchange(a);
System.out.println("a------" + a); // 10
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
new Thread(() -> {
try {
int b = 10;
b = exchanger.exchange(b);
System.out.println("b-----" + b); // 3
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
}
9.synchronized
#sychronized 應(yīng)用方式
>> 1.作用于實(shí)例方法驶臊,當(dāng)前實(shí)例加鎖挪挤,進(jìn)入同步代碼塊時(shí)需要獲得當(dāng)前實(shí)例的鎖;
>> 2.作用于靜態(tài)方法关翎,當(dāng)前類(lèi)加鎖扛门,進(jìn)去同步代碼前需要獲取當(dāng)前類(lèi)的鎖;
>> 3.作用于代碼塊纵寝,這需要指定加鎖對(duì)象论寨,對(duì)所給的指定對(duì)象加鎖,進(jìn)入同步代碼塊前要獲得指定對(duì)象的鎖爽茴;
9.3 sychronized底層原理
java虛擬機(jī)中的同步Synchronization基于進(jìn)入和退出管程Monitor對(duì)象實(shí)現(xiàn)葬凳。
在java中,sychronized可以修飾同步方法室奏,
同步方法不是由monitorenter和moniterexit指令來(lái)實(shí)現(xiàn)同步火焰,而是由方法調(diào)用指令讀取運(yùn)行時(shí)常量池中的ACC_SYNCHRONIZED標(biāo)注來(lái)隱式調(diào)用的。
在java中胧沫,對(duì)象在內(nèi)存中的布局分為三塊區(qū)域昌简,對(duì)象頭,實(shí)例數(shù)據(jù)和填充數(shù)據(jù)琳袄。
#1江场、對(duì)象頭
HotSpot虛擬機(jī)的對(duì)象頭包括markword和klass,數(shù)組長(zhǎng)度窖逗;
>> markword
用于存儲(chǔ)對(duì)象自身的運(yùn)行時(shí)數(shù)據(jù)址否,如哈希碼(HashCode)、
GC分代年齡碎紊、鎖狀態(tài)標(biāo)志佑附、線(xiàn)程持有的鎖、偏向線(xiàn)程ID仗考、偏向時(shí)間戳等音同,
這部分?jǐn)?shù)據(jù)的長(zhǎng)度在32位和64位的虛擬機(jī)(未開(kāi)啟壓縮指針)中分別為32bit和64bit。
>> klass類(lèi)型指針
即對(duì)象指向它的類(lèi)元數(shù)據(jù)的指針秃嗜,虛擬機(jī)通過(guò)這個(gè)指針來(lái)確定這個(gè)對(duì)象是哪個(gè)類(lèi)的實(shí)例权均。
>> 數(shù)組長(zhǎng)度
如果對(duì)象是一個(gè)數(shù)組, 那在對(duì)象頭中還必須有一塊數(shù)據(jù)用于記錄數(shù)組長(zhǎng)度顿膨。
#2、實(shí)例數(shù)據(jù)
實(shí)例數(shù)據(jù)部分是對(duì)象真正存儲(chǔ)的有效信息叽赊,也是在程序代碼中所定義的各種類(lèi)型的字段內(nèi)容恋沃。
無(wú)論是從父類(lèi)繼承下來(lái)的,還是在子類(lèi)中定義的必指,都需要記錄起來(lái)囊咏。
#3、對(duì)象填充
對(duì)齊填充并不是必然存在的塔橡,也沒(méi)有特別的含義梅割,它僅僅起著占位符的作用。
由于HotSpot VM的自動(dòng)內(nèi)存管理系統(tǒng)要求對(duì)象起始地址必須是8字節(jié)的整數(shù)倍葛家,
換句話(huà)說(shuō)户辞,就是對(duì)象的大小必須是8字節(jié)的整數(shù)倍。
而對(duì)象頭部分正好是8字節(jié)的倍數(shù)(1倍或者2倍)惦银,
因此咆课,當(dāng)對(duì)象實(shí)例數(shù)據(jù)部分沒(méi)有對(duì)齊時(shí),就需要通過(guò)對(duì)齊填充來(lái)補(bǔ)全扯俱。
重量級(jí)鎖就是sychronized的對(duì)象鎖书蚪,鎖標(biāo)識(shí)為10,其中指針指向的是monitor對(duì)象的起始地址迅栅。
每個(gè)對(duì)象都存在著一個(gè)monitor與之關(guān)聯(lián)殊校,對(duì)象與其monitor之間的關(guān)系有存在多重實(shí)現(xiàn)方式,
如monitor可以與對(duì)象一起創(chuàng)建銷(xiāo)毀或線(xiàn)程試圖獲取對(duì)象鎖時(shí)自動(dòng)生成读存,
但當(dāng)一個(gè)monitor被某個(gè)線(xiàn)程持有后为流,它便處于鎖定狀態(tài)。
在java虛擬機(jī)中(hotSpot)让簿,monitor是由ObjectMonitor實(shí)現(xiàn)的敬察。
ObjectMonitor中有兩個(gè)隊(duì)列,_WaitSet和_EntryList尔当,用來(lái)保存ObjectWaiter列表莲祸,
每個(gè)等待鎖的線(xiàn)程都會(huì)封裝成ObjectWaiter對(duì)象,_owner指向持有ObjectMonitor對(duì)象的線(xiàn)程椭迎,
當(dāng)多個(gè)線(xiàn)程同時(shí)訪(fǎng)問(wèn)同一段同步代碼時(shí)锐帜,首先會(huì)進(jìn)入_EntryList集合,
當(dāng)線(xiàn)程獲取到對(duì)象的monitor后進(jìn)入_Owner區(qū)域并把monitor中的_owner變量設(shè)置為當(dāng)前線(xiàn)程畜号,
同時(shí)monitor的計(jì)數(shù)器_count加1缴阎,若先寫(xiě)調(diào)用wait()方法,
將釋放當(dāng)前持有的monitor,_owner變量恢復(fù)為null简软,count自減1蛮拔,
同時(shí)該線(xiàn)程進(jìn)入_waitSet集合等待被喚醒述暂。
若當(dāng)前線(xiàn)程執(zhí)行完畢也將釋放monitor,并復(fù)位變量的值语泽,以便于其他線(xiàn)程獲取monitor鎖贸典。
monitor對(duì)象存在每個(gè)java對(duì)象的對(duì)象頭中(存儲(chǔ)的指針的指向),
synchronized鎖便是用過(guò)這種方式獲取鎖的视卢,這也是為什么java的任意對(duì)象都可以作為鎖的原因踱卵,
同時(shí)也是notify/notifyAll/wait方法存在object方法中的原因。
9.4 同步方法的實(shí)現(xiàn)原理
同步方法和靜態(tài)同步方法是依賴(lài)方法修飾符ACC_SYNCHRONIZED實(shí)現(xiàn)据过,
當(dāng)方法調(diào)用時(shí)惋砂,調(diào)用指令將會(huì)檢查方法的ACC_SYNCHRONIZED訪(fǎng)問(wèn)標(biāo)志是否被設(shè)置,
如果設(shè)置了绳锅,執(zhí)行時(shí)將先獲取monitor西饵,獲取成功夠才能執(zhí)行方法體,
方法執(zhí)行完成后再釋放monitor鳞芙,在方法執(zhí)行期間眷柔,其他任何線(xiàn)程都無(wú)法在獲得同一個(gè)monitor對(duì)象。
9.5 同步代碼塊的實(shí)現(xiàn)原理
同步代碼塊是使用monitorenter和moniterexist指令實(shí)現(xiàn)的原朝,
會(huì)在同步塊的區(qū)域通過(guò)監(jiān)聽(tīng)器對(duì)象去獲取鎖和釋放鎖驯嘱。
9.6 JDK1.6后的sychronized關(guān)鍵字底層做了哪些優(yōu)化?
引入了偏向鎖喳坠、輕量級(jí)鎖鞠评、自旋鎖、適應(yīng)性自旋鎖壕鹉、鎖消除剃幌、鎖粗化等技術(shù)減少所操作的開(kāi)銷(xiāo)。
#鎖主要存在四種狀態(tài):
無(wú)鎖狀態(tài)晾浴,偏向鎖狀態(tài)负乡,輕量級(jí)鎖狀態(tài),重量級(jí)鎖狀態(tài)脊凰,
鎖可以升級(jí)不能降級(jí)抖棘,這種策略是為了提高獲得鎖和釋放鎖的效率。
? 一個(gè)對(duì)象里面如果有多個(gè)synchronized方法笙各,某一個(gè)時(shí)刻內(nèi)钉答,只要一個(gè)線(xiàn)程去調(diào)用
其中的一個(gè)synchronized方法了,其它的線(xiàn)程都只能等待杈抢,換句話(huà)說(shuō)数尿,某一個(gè)時(shí)刻
內(nèi),只能有唯一一個(gè)線(xiàn)程去訪(fǎng)問(wèn)這些synchronized方法
? 鎖的是當(dāng)前對(duì)象this惶楼,被鎖定后右蹦,
其它的線(xiàn)程都不能進(jìn)入到當(dāng)前對(duì)象的其它的synchronized方法
? 加個(gè)普通方法后發(fā)現(xiàn)和同步鎖無(wú)關(guān)
? 換成兩個(gè)對(duì)象后诊杆,不是同一把鎖了,情況立刻變化何陆。
? 都換成靜態(tài)同步方法后晨汹,情況又變化
? 所有的非靜態(tài)同步方法用的都是同一把鎖——實(shí)例對(duì)象本身(this),也就是說(shuō)如果一個(gè)實(shí)
例對(duì)象的非靜態(tài)同步方法獲取鎖后贷盲,該實(shí)例對(duì)象的其他非靜態(tài)同步方法必須等待獲
取鎖的方法釋放鎖后才能獲取鎖淘这,可是別的實(shí)例對(duì)象的非靜態(tài)同步方法因?yàn)楦搶?shí)
例對(duì)象的非靜態(tài)同步方法用的是不同的鎖,所以毋須等待該實(shí)例對(duì)象已獲取鎖的非
靜態(tài)同步方法釋放鎖就可以獲取他們自己的鎖巩剖。
? 所有的靜態(tài)同步方法用的也是同一把鎖——類(lèi)對(duì)象本身(XXX.Class)铝穷,這兩把鎖是兩個(gè)不同的對(duì)
象,所以靜態(tài)同步方法與非靜態(tài)同步方法之間是不會(huì)有競(jìng)態(tài)條件的佳魔。但是一旦一個(gè)
靜態(tài)同步方法獲取鎖后曙聂,其他的靜態(tài)同步方法都必須等待該方法釋放鎖后才能獲取
鎖,而不管是同一個(gè)實(shí)例對(duì)象的靜態(tài)同步方法之間鞠鲜,還是不同的實(shí)例對(duì)象的靜態(tài)同
步方法之間宁脊,只要它們同一個(gè)類(lèi)的實(shí)例對(duì)象!
10.synchronized與Lock的區(qū)別與聯(lián)系
10.1 區(qū)別
#a.原始構(gòu)成
synchronized: 屬于JVM層面的鎖, JDK1.6 為 synchronized 關(guān)鍵字進(jìn)行了很多的優(yōu)化, monitorenter, monitorexit
Lock: 是API層面的鎖
#b.使用方法
synchronized:
>> 不需要手動(dòng)釋放鎖
>> 結(jié)合 wait() 和 notify()/notifyAll() 方法使用贤姆,可以實(shí)現(xiàn)等待/通知機(jī)制
Lock:
>> 需要手動(dòng)去釋放鎖, 放在try..finally..塊中, 否則會(huì)死鎖
>> 需要借助于 Condition 接口與 newCondition() 方法榆苞。
#c.等待是否可中斷
synchronized: 不可中斷, 除非拋出異常或任務(wù)完成
ReentrantLock: 可中斷:(設(shè)置超時(shí)時(shí)間lock.tryLock(), 或直接中斷l(xiāng)ock.lockInterruptibly())
#d. 鎖是否公平
synchronized: 是非公平鎖
ReentrantLock: 默認(rèn)非公平鎖, 支持公平和非公平鎖
#e. 鎖綁定多個(gè)條件
synchronized沒(méi)有, 只能要么全部喚醒, 要么喚醒一個(gè)
ReentrantLock用來(lái)實(shí)現(xiàn)分組喚醒需要喚醒的線(xiàn)程們, 可以精確喚醒,
10.2 相同點(diǎn)
#1.兩者都是可重入鎖
兩者都是同一個(gè)線(xiàn)程沒(méi)進(jìn)入一次庐氮,鎖的計(jì)數(shù)器都自增1语稠,
所以要等到鎖的計(jì)數(shù)器下降為0時(shí)才能釋放鎖。
11.死鎖
11.1 死鎖概念
package com.zy.tools.undefined.concurrent.deadLock;
public class DeadLockDemo {
public static void main(String[] args) {
LockDemo demoA = new LockDemo(true);
LockDemo demoB = new LockDemo(false);
new Thread(demoA, "demoA").start();
new Thread(demoB, "demoB").start();
}
}
class LockDemo implements Runnable {
private final static Object lockA = new Object();
private final static Object lockB = new Object();
private boolean isA;
public LockDemo(boolean isA) {
this.isA = isA;
}
@Override
public void run() {
if (isA) {
methodA();
} else {
methodB();
}
}
public void methodA() {
synchronized (lockA) {
System.out.println(Thread.currentThread().getName() + "獲取lockA, 嘗試獲得lockB");
synchronized (lockB) {
System.out.println(Thread.currentThread().getName() + "獲取lockB");
}
}
}
public void methodB() {
synchronized (lockB) {
System.out.println(Thread.currentThread().getName() + "獲取lockB, 嘗試獲得lockA");
synchronized (lockA) {
System.out.println(Thread.currentThread().getName() + "獲取lockA");
}
}
}
}
# 分析思路:
1.在當(dāng)前代碼工程下, 執(zhí)行: jps -l, 找到可疑的進(jìn)程號(hào)如14060
2.執(zhí)行jstack 14060, 查看是否有死鎖現(xiàn)象
11.2 死鎖解決方案
死鎖的影響在不同系統(tǒng)中是不一樣的弄砍,這取決于系統(tǒng)對(duì)死鎖的處理能力
>> 數(shù)據(jù)庫(kù)中:檢測(cè)并放棄事務(wù)
>> JVM中:無(wú)法自動(dòng)處理
11.2.1 多人互相轉(zhuǎn)賬
#死鎖原因
>> 需要兩把鎖
>> 獲取兩把鎖成功仙畦,且余額大于0,則扣除轉(zhuǎn)出人音婶,增加收款人的余額慨畸,是原子操作
>> 順序相反導(dǎo)致死鎖
#解決方案1 ==> System.identityHashCode()
其實(shí)轉(zhuǎn)賬時(shí),并不在乎兩把鎖的相對(duì)獲取順序寸士。
轉(zhuǎn)賬的時(shí)候婶博,我們無(wú)論先獲取到轉(zhuǎn)出賬戶(hù)鎖對(duì)象挠轴,還是先獲取到轉(zhuǎn)入賬戶(hù)鎖對(duì)象,只要最終能拿到兩把鎖,就能進(jìn)行安全的操作讥邻。
所以我們來(lái)調(diào)整一下獲取鎖的順序发魄,使得先獲取的賬戶(hù)和該賬戶(hù)是“轉(zhuǎn)入”或“轉(zhuǎn)出”無(wú)關(guān),
而是使用 HashCode 的值來(lái)決定順序,從而保證線(xiàn)程安全找默。
#解決方案2 ==> 死鎖檢測(cè)與恢復(fù)策略
>> 允許發(fā)生死鎖
>> 每次調(diào)用鎖的記錄
>> 定期檢查"鎖的調(diào)用鏈路圖"中是否存在環(huán)路
>> 一旦發(fā)生死鎖浅缸,就用死鎖恢復(fù)機(jī)制進(jìn)行恢復(fù)機(jī)制進(jìn)行恢復(fù)
12.分布式鎖
http://www.reibang.com/p/e4eb43573f84 (參考此文)
13.活鎖
假設(shè)有兩個(gè)線(xiàn)程t1, t2苟弛,它們都需要資源 A/B,假設(shè)t1占有了 A 資源图筹,t2占有了 B 資源娇妓;
由于兩個(gè)線(xiàn)程都需要同時(shí)擁有這兩個(gè)資源才可以工作锌云,為了避免死鎖,t1釋放了 A 資源占有鎖娃胆,t2釋放了 B 資源占有鎖遍希;
此時(shí) AB 空閑,兩個(gè)線(xiàn)程又同時(shí)搶鎖里烦,再次出現(xiàn)上述情況凿蒜,此時(shí)發(fā)生了活鎖。
簡(jiǎn)單類(lèi)比胁黑,電梯遇到人废封,一個(gè)進(jìn)的一個(gè)出的,對(duì)面占路丧蘸,兩個(gè)人同時(shí)往一個(gè)方向讓路漂洋,來(lái)回重復(fù),還是堵著路触趴。
活鎖問(wèn)題氮发,比較難排查。
14.饑餓
一般非公平鎖時(shí), 可能出現(xiàn)鎖饑餓情況.
饑餓是指某一個(gè)或者多個(gè)線(xiàn)程因?yàn)榉N種原因無(wú)法獲得所需要的資源冗懦,導(dǎo)致一直無(wú)法執(zhí)行。
參考鏈接
https://www.cnblogs.com/waterystone/p/4920797.html (AQS)
https://blog.csdn.net/qq_29519041/article/details/86583945 (可重入鎖)
https://www.cnblogs.com/incognitor/p/9894604.html (可重入鎖)
https://segmentfault.com/a/1190000015808032?utm_source=tag-newest (juc鎖綜合)
https://segmentfault.com/a/1190000007094429 (CLH Lock)
http://www.cs.tau.ac.il/~shanir/nir-pubs-web/Papers/CLH.pdf (CLH Lock)
http://www.reibang.com/p/d35a6a1fd810 (常見(jiàn)死鎖)