首先說起一下前置知識:java
1.JAVA併發之基礎概念算法
2.JAVA併發之進程VS線程編程
在前三章咱們討論了多線程併發
的優勢以及如何加鎖來處理併發帶來的安全性問題
數據結構
可是加鎖也爲咱們帶來了諸多問題 如:死鎖,活鎖,線程飢餓等問題 這一章我咱們主要處理鎖帶來的問題. 首先就是最出名的死鎖多線程
1.死鎖(Deadlock)
什麼是死鎖併發
死鎖是當線程進入無限期等待狀態時發生的狀況,由於所請求的鎖被另外一個線程持有,而另外一個線程又等待第一個線程持有的另外一個鎖 致使互相等待。總結:多個線程互相等待對方釋放鎖。分佈式
例如在現實中的十字路口,鎖就像紅路燈指示器,一旦鎖壞了,就會致使交通癱瘓。 那麼該如何避免這個問題呢ide
死鎖的解決和預防
1.超時釋放鎖性能
>顧名思義,這種避免死鎖的方式是在嘗試獲取鎖的時候加一個超時時間,這就意味着,若是一個線程在獲取鎖的門口等待過久這個線程就會放棄此次請求,退還並釋放全部已經得到的鎖,再在等待一段隨機時間後再次嘗試,這段時間其餘的線程夥伴能夠去嘗試拿鎖.
public interface Lock { //自定義異常類 public static class TimeOutException extends Exception{ public TimeOutException(String message){ super(message); } } //無超時鎖,能夠被打斷 void lock() throws InterruptedException; //超時鎖,能夠被打斷 void lock(long molls) throws InterruptedException,TimeOutException; //解鎖 void unlock(); //獲取當前等待的線程 Collection<Thread> getBlockedThread(); //獲取當前阻塞的線程數目 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(); } //代表此時正在用,別人進來就要鎖住 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; //代表鎖已經釋放 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流的方式建立四個線程 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(); }); } //若是是須要一直等待就調用 lock(),若是是超時要退出來就調用超時lock(long millo) private static void work() throws InterruptedException{ Optional.of(Thread.currentThread().getName()+" is working.....'").ifPresent(System.out::println); Thread.sleep(40_000); } }
運行:
T1 have the lock Monitor T1 is working..... T2 time out T4 time out T3 time out
2.按順序加鎖
>按照順序加鎖是一種有效防止死鎖的機制,可是這種方式,你須要先知道全部可能用到鎖的位置,並對這些鎖安排一個順序
3.死鎖檢測
>死鎖檢測是一個更好的死鎖預防機制,主要用於超時鎖和按順序加鎖不可用的場景每當一個線程得到了鎖,會在線程和鎖相關的數據結構中(map、graph 等等)將其記下。除此以外,每當有線程請求鎖,也須要記錄在這個數據結構中。當一個線程請求鎖失敗時,這個線程能夠遍歷鎖的關係圖看看是否有死鎖發生。
若是檢測出死鎖,有兩種處理手段:
- 釋放全部鎖,回退,而且等待一段隨機的時間後重試。這個和簡單的加鎖超時相似,不同的是隻有死鎖已經發生了纔回退,而不會是由於加鎖的請求超時了。雖然有回退和等待,可是若是有大量的線程競爭同一批鎖,它們仍是會重複地死鎖,緣由同超時相似,不能從根本上減輕競爭.
- 一個更好的方案是給這些線程設置優先級,讓一個(或幾個)線程回退,剩下的線程就像沒發生死鎖同樣繼續保持着它們須要的鎖。若是賦予這些線程的優先級是固定不變的,同一批線程老是會擁有更高的優先級。爲避免這個問題,能夠在死鎖發生的時候設置隨機的優先級。
2.活鎖(Livelock)
什麼是活鎖
死鎖是一直死等,活鎖他不死等,它會一直執行,可是線程就是不能繼續,由於它不斷重試相同的操做。換句話說,就是信息處理線程並無發生阻塞,可是永遠都不會前進了,當他們爲了彼此間的響應而相互禮讓,使得沒有一個線程可以繼續前進,那麼就發生了活鎖
避免活鎖
解決「活鎖」的方案很簡單,謙讓時,嘗試等待一個隨機的時間就能夠了。因爲等待的時間是隨機的,因此同時相撞後再次相撞的機率就很低了。「等待一個隨機時間」的方案雖然很簡單,卻很是有效,Raft 這樣知名的分佈式一致性算法中也用到了它。
3.飢餓
什麼是飢餓
- 高優先級線程吞噬全部的低優先級線程的 CPU 時間。
- 線程被永久堵塞在一個等待進入同步塊的狀態,由於其餘線程老是能在它以前持續地對該同步塊進行訪問。
- 線程在等待一個自己(在其上調用 wait())也處於永久等待完成的對象,由於其餘線程老是被持續地得到喚醒。
飢餓問題最經典的例子就是哲學家問題。如圖所示:有五個哲學家用餐,每一個人要活得兩把叉子才能夠就餐。當 二、4 就餐時,一、三、5 永遠沒法就餐,只能看着盤中的美食飢餓的等待着。
解決飢餓
Java 不可能實現 100% 的公平性,咱們依然能夠經過同步結構在線程間實現公平性的提升。
有三種方案:
- 保證資源充足
- 公平地分配資源
- 避免持有鎖的線程長時間執行
這三個方案中,方案一和方案三的適用場景比較有限,由於不少場景下,資源的稀缺性是沒辦法解決的,持有鎖的線程執行的時間也很難縮短。卻是方案二的適用場景相對來講更多一些。 那如何公平地分配資源呢?在併發編程裏,主要是使用公平鎖。所謂公平鎖,是一種先來後到的方案,線程的等待是有順序的,排在等待隊列前面的線程會優先得到資源。
4.性能問題
併發執行必定比串行執行快嗎?線程越多執行越快嗎?
答案是:併發不必定比串行快。由於有建立線程和線程上下文切換
的開銷。
5.上下文切換
什麼是上下文切換?
當 CPU 從執行一個線程切換到執行另外一個線程時,CPU 須要保存當前線程的本地數據,程序指針等狀態,並加載下一個要執行的線程的本地數據,程序指針等。這個開關被稱爲「上下文切換」。
減小上下文切換的方法
- 無鎖併發編程 - 多線程競爭鎖時,會引發上下文切換,因此多線程處理數據時,能夠用一些辦法來避免使用鎖,如將數據的 ID 按照 Hash 算法取模分段,不一樣的線程處理不一樣段的數據。
- CAS 算法 - Java 的 Atomic 包使用 CAS 算法來更新數據,而不須要加鎖。
- 使用最少線程 - 避免建立不須要的線程,好比任務不多,可是建立了不少線程來處理,這樣會形成大量線程都處於等待狀態。
- 使用協程 - 在單線程裏實現多任務的調度,並在單線程裏維持多個任務間的切換。
6.資源限制
什麼是資源限制
資源限制是指在進行併發編程時,程序的執行速度受限於計算機硬件資源或軟件資源。
資源限制引起的問題
在併發編程中,將代碼執行速度加快的原則是將代碼中串行執行的部分變成併發執行,可是若是將某段串行的代碼併發執行,由於受限於資源,仍然在串行執行,這時候程序不只不會加快執行,反而會更慢,由於增長了上下文切換和資源調度的時間。
如何解決資源限制的問題
在資源限制狀況下進行併發編程,根據不一樣的資源限制調整程序的併發度。
- 對於硬件資源限制,能夠考慮使用集羣並行執行程序。
- 對於軟件資源限制,能夠考慮使用資源池將資源複用。
總結
至本章爲止,多線程併發的概念篇就結束了,實際操做篇盡情期待 持續關注公衆號 JAVA寶典
關注公衆號:java寶典