Java并發(fā)編程加鎖導(dǎo)致的活躍性問題詳解方案
我們主要處理鎖帶來的問題.
首先就是最出名的死鎖
死鎖(Deadlock)
什么是死鎖
死鎖是當(dāng)線程進(jìn)入無限期等待狀態(tài)時(shí)發(fā)生的情況,因?yàn)樗?qǐng)求的鎖被另一個(gè)線程持有,而另一個(gè)線程又等待第一個(gè)線程持有的另一個(gè)鎖 導(dǎo)致互相等待??偨Y(jié):多個(gè)線程互相等待對(duì)方釋放鎖。
例如在現(xiàn)實(shí)中的十字路口,鎖就像紅綠燈指示器,一旦鎖壞了,就會(huì)導(dǎo)致交通癱瘓。
那么該如何避免這個(gè)問題呢
死鎖的解決和預(yù)防
1.超時(shí)釋放鎖
>顧名思義,這種避免死鎖的方式是在嘗試獲取鎖的時(shí)候加一個(gè)超時(shí)時(shí)間,這就意味著,如果一個(gè)線程在獲取鎖的門口等待太久這個(gè)線程就會(huì)放棄這次請(qǐng)求,退還并釋放所有已經(jīng)獲得的鎖,再在等待一段隨機(jī)時(shí)間后再次嘗試,這段時(shí)間其他的線程伙伴可以去嘗試拿鎖.
public interface Lock { //自定義異常類 public static class TimeOutException extends Exception{ public TimeOutException(String message){ super(message); } } //無超時(shí)鎖,可以被打斷 void lock() throws InterruptedException; //超時(shí)鎖,可以被打斷 void lock(long molls) throws InterruptedException,TimeOutException; //解鎖 void unlock(); //獲取當(dāng)前等待的線程 Collection<Thread> getBlockedThread(); //獲取當(dāng)前阻塞的線程數(shù)目 int getBlockSize(); }
public class BooleanLock implements Lock{ private boolean initValue; private Thread currenThread; public BooleanLock(){ this.initValue = false; } private Collection<Thread> blockThreadCollection = new ArrayList<>(); @Override public synchronized void lock() throws InterruptedException { while (initValue){ blockThreadCollection.add(Thread.currentThread()); this.wait(); } //表明此時(shí)正在用,別人進(jìn)來就要鎖住 this.initValue = true; currenThread = Thread.currentThread(); blockThreadCollection.remove(Thread.currentThread());//從集合中刪除 } @Override public synchronized void lock(long mills) throws InterruptedException, TimeOutException { if (mills<=0){ lock(); }else { long hasRemain = mills; long endTime = System.currentTimeMillis()+mills; while (initValue){ if (hasRemain<=0) throw new TimeOutException("Time out"); blockThreadCollection.add(Thread.currentThread()); hasRemain = endTime-System.currentTimeMillis(); } this.initValue = true; currenThread = Thread.currentThread(); } } @Override public synchronized void unlock() { if (currenThread==Thread.currentThread()){ this.initValue = false; //表明鎖已經(jīng)釋放 Optional.of(Thread.currentThread().getName()+ " release the lock monitor").ifPresent(System.out::println); this.notifyAll(); } } @Override public Collection<Thread> getBlockedThread() { return Collections.unmodifiableCollection(blockThreadCollection); } @Override public int getBlockSize() { return blockThreadCollection.size(); } }
public class BlockTest { public static void main(String[] args) throws InterruptedException { final BooleanLock booleanLock = new BooleanLock(); // 使用Stream流的方式創(chuàng)建四個(gè)線程 Stream.of("T1","T2","T3","T4").forEach(name->{ new Thread(()->{ try { booleanLock.lock(10); Optional.of(Thread.currentThread().getName()+" have the lock Monitor").ifPresent(System.out::println); work(); } catch (InterruptedException e) { e.printStackTrace(); } catch (Lock.TimeOutException e) { Optional.of(Thread.currentThread().getName()+" time out").ifPresent(System.out::println); } finally { booleanLock.unlock(); } },name).start(); }); } //如果是需要一直等待就調(diào)用 lock(),如果是超時(shí)要退出來就調(diào)用超時(shí)lock(long millo) private static void work() throws InterruptedException{ Optional.of(Thread.currentThread().getName()+" is working.....'").ifPresent(System.out::println); Thread.sleep(40_000); } }
運(yùn)行:
T1 have the lock Monitor
T1 is working.....
T2 time out
T4 time out
T3 time out
2.按順序加鎖
>按照順序加鎖是一種有效防止死鎖的機(jī)制,但是這種方式,你需要先知道所有可能用到鎖的位置,并對(duì)這些鎖安排一個(gè)順序
3.死鎖檢測(cè)
>死鎖檢測(cè)是一個(gè)更好的死鎖預(yù)防機(jī)制,主要用于超時(shí)鎖和按順序加鎖不可用的場(chǎng)景每當(dāng)一個(gè)線程獲得了鎖,會(huì)在線程和鎖相關(guān)的數(shù)據(jù)結(jié)構(gòu)中(map、graph 等等)將其記下。除此之外,每當(dāng)有線程請(qǐng)求鎖,也需要記錄在這個(gè)數(shù)據(jù)結(jié)構(gòu)中。當(dāng)一個(gè)線程請(qǐng)求鎖失敗時(shí),這個(gè)線程可以遍歷鎖的關(guān)系圖看看是否有死鎖發(fā)生。
如果檢測(cè)出死鎖,有兩種處理手段:
- 釋放所有鎖,回退,并且等待一段隨機(jī)的時(shí)間后重試。這個(gè)和簡(jiǎn)單的加鎖超時(shí)類似,不一樣的是只有死鎖已經(jīng)發(fā)生了才回退,而不會(huì)是因?yàn)榧渔i的請(qǐng)求超時(shí)了。雖然有回退和等待,但是如果有大量的線程競(jìng)爭(zhēng)同一批鎖,它們還是會(huì)重復(fù)地死鎖,原因同超時(shí)類似,不能從根本上減輕競(jìng)爭(zhēng).
- 一個(gè)更好的方案是給這些線程設(shè)置優(yōu)先級(jí),讓一個(gè)(或幾個(gè))線程回退,剩下的線程就像沒發(fā)生死鎖一樣繼續(xù)保持著它們需要的鎖。如果賦予這些線程的優(yōu)先級(jí)是固定不變的,同一批線程總是會(huì)擁有更高的優(yōu)先級(jí)。為避免這個(gè)問題,可以在死鎖發(fā)生的時(shí)候設(shè)置隨機(jī)的優(yōu)先級(jí)。
活鎖(Livelock)
什么是活鎖
死鎖是一直死等,活鎖他不死等,它會(huì)一直執(zhí)行,但是線程就是不能繼續(xù),因?yàn)樗粩嘀卦囅嗤牟僮?。換句話說,就是信息處理線程并沒有發(fā)生阻塞,但是永遠(yuǎn)都不會(huì)前進(jìn)了,當(dāng)他們?yōu)榱吮舜碎g的響應(yīng)而相互禮讓,使得沒有一個(gè)線程能夠繼續(xù)前進(jìn),那么就發(fā)生了活鎖
避免活鎖
解決“活鎖”的方案很簡(jiǎn)單,謙讓時(shí),嘗試等待一個(gè)隨機(jī)的時(shí)間就可以了。由于等待的時(shí)間是隨機(jī)的,所以同時(shí)相撞后再次相撞的概率就很低了?!暗却粋€(gè)隨機(jī)時(shí)間”的方案雖然很簡(jiǎn)單,卻非常有效,Raft 這樣知名的分布式一致性算法中也用到了它。
饑餓
什么是饑餓
高優(yōu)先級(jí)線程吞噬所有的低優(yōu)先級(jí)線程的 CPU 時(shí)間。線程被永久堵塞在一個(gè)等待進(jìn)入同步塊的狀態(tài),因?yàn)槠渌€程總是能在它之前持續(xù)地對(duì)該同步塊進(jìn)行訪問。線程在等待一個(gè)本身(在其上調(diào)用 wait())也處于永久等待完成的對(duì)象,因?yàn)槠渌€程總是被持續(xù)地獲得喚醒。
饑餓問題最經(jīng)典的例子就是哲學(xué)家問題。如圖所示:有五個(gè)哲學(xué)家用餐,每個(gè)人要活得兩把叉子才可以就餐。當(dāng) 2、4 就餐時(shí),1、3、5 永遠(yuǎn)無法就餐,只能看著盤中的美食饑餓的等待著。
解決饑餓
Java 不可能實(shí)現(xiàn) 100% 的公平性,我們依然可以通過同步結(jié)構(gòu)在線程間實(shí)現(xiàn)公平性的提高。
有三種方案:
保證資源充足公平地分配資源避免持有鎖的線程長(zhǎng)時(shí)間執(zhí)行
這三個(gè)方案中,方案一和方案三的適用場(chǎng)景比較有限,因?yàn)楹芏鄨?chǎng)景下,資源的稀缺性是沒辦法解決的,持有鎖的線程執(zhí)行的時(shí)間也很難縮短。倒是方案二的適用場(chǎng)景相對(duì)來說更多一些。
那如何公平地分配資源呢?在并發(fā)編程里,主要是使用公平鎖。所謂公平鎖,是一種先來后到的方案,線程的等待是有順序的,排在等待隊(duì)列前面的線程會(huì)優(yōu)先獲得資源。
性能問題
并發(fā)執(zhí)行一定比串行執(zhí)行快嗎?線程越多執(zhí)行越快嗎?
答案是:并發(fā)不一定比串行快。因?yàn)橛袆?chuàng)建線程和線程上下文切換的開銷。
上下文切換
什么是上下文切換?
當(dāng) CPU 從執(zhí)行一個(gè)線程切換到執(zhí)行另一個(gè)線程時(shí),CPU 需要保存當(dāng)前線程的本地?cái)?shù)據(jù),程序指針等狀態(tài),并加載下一個(gè)要執(zhí)行的線程的本地?cái)?shù)據(jù),程序指針等。這個(gè)開關(guān)被稱為“上下文切換”。
減少上下文切換的方法
無鎖并發(fā)編程 - 多線程競(jìng)爭(zhēng)鎖時(shí),會(huì)引起上下文切換,所以多線程處理數(shù)據(jù)時(shí),可以用一些辦法來避免使用鎖,如將數(shù)據(jù)的 ID 按照 Hash 算法取模分段,不同的線程處理不同段的數(shù)據(jù)。CAS 算法 - Java 的 Atomic 包使用 CAS 算法來更新數(shù)據(jù),而不需要加鎖。使用最少線程 - 避免創(chuàng)建不需要的線程,比如任務(wù)很少,但是創(chuàng)建了很多線程來處理,這樣會(huì)造成大量線程都處于等待狀態(tài)。使用協(xié)程 - 在單線程里實(shí)現(xiàn)多任務(wù)的調(diào)度,并在單線程里維持多個(gè)任務(wù)間的切換。
資源限制
什么是資源限制
資源限制是指在進(jìn)行并發(fā)編程時(shí),程序的執(zhí)行速度受限于計(jì)算機(jī)硬件資源或軟件資源。
資源限制引發(fā)的問題
在并發(fā)編程中,將代碼執(zhí)行速度加快的原則是將代碼中串行執(zhí)行的部分變成并發(fā)執(zhí)行,但是如果將某段串行的代碼并發(fā)執(zhí)行,因?yàn)槭芟抻谫Y源,仍然在串行執(zhí)行,這時(shí)候程序不僅不會(huì)加快執(zhí)行,反而會(huì)更慢,因?yàn)樵黾恿松舷挛那袚Q和資源調(diào)度的時(shí)間。
如何解決資源限制的問題
在資源限制情況下進(jìn)行并發(fā)編程,根據(jù)不同的資源限制調(diào)整程序的并發(fā)度。
對(duì)于硬件資源限制,可以考慮使用集群并行執(zhí)行程序。對(duì)于軟件資源限制,可以考慮使用資源池將資源復(fù)用。
到此這篇關(guān)于Java并發(fā)編程加鎖導(dǎo)致的活躍性問題詳解方案的文章就介紹到這了,更多相關(guān)Java 并發(fā)編程加鎖內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
基于Java并發(fā)容器ConcurrentHashMap#put方法解析
下面小編就為大家?guī)硪黄贘ava并發(fā)容器ConcurrentHashMap#put方法解析。小編覺得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2017-06-06Java使用選擇排序法對(duì)數(shù)組排序?qū)崿F(xiàn)代碼
這篇文章主要介紹了Java使用選擇排序法對(duì)數(shù)組排序?qū)崿F(xiàn)代碼,需要的朋友可以參考下2014-02-02Springboot定時(shí)任務(wù)Scheduled重復(fù)執(zhí)行操作
這篇文章主要介紹了Springboot定時(shí)任務(wù)Scheduled重復(fù)執(zhí)行操作,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2020-09-09JavaScript中HTML元素操作的實(shí)現(xiàn)
本文主要介紹了JavaScript中HTML元素操作的實(shí)現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2023-06-06Java數(shù)據(jù)類型實(shí)現(xiàn)自動(dòng)與強(qiáng)制轉(zhuǎn)換的示例代碼
Java數(shù)據(jù)類型之間的轉(zhuǎn)換有自動(dòng)轉(zhuǎn)換和強(qiáng)制類型轉(zhuǎn)換,這篇文章主要給大家介紹Java數(shù)據(jù)類型如何實(shí)現(xiàn)自動(dòng)轉(zhuǎn)換與強(qiáng)制轉(zhuǎn)換,需要的朋友可以參考下2023-05-05SpringBoot啟動(dòng)失敗的解決方法:A component required a&nb
這篇文章主要介紹了解決SpringBoot啟動(dòng)失敗:A component required a bean of type ‘xxxxxxx‘ that could not be found.,目前解決方法有兩種,一種是不注入bean的方式,另一種是使用@Component的方式,本文給大家詳細(xì)講解,需要的朋友可以參考下2023-02-02