Java多線程中Lock鎖的使用總結(jié)
多核時代
摩爾定律告訴我們:當(dāng)價格不變時,集成電路上可容納的晶體管數(shù)目,約每隔18個月便會增加一倍,性能也將提升一倍。換言之,每一美元所能買到的電腦性能,將每隔18個月翻兩倍以上。然而最近摩爾定律似乎遇到了麻煩,目前微處理器的集成度似乎到了極限,在目前的制造工藝和體系架構(gòu)下很難再提高單個處理器的速度了,否則它就被燒壞了。所以現(xiàn)在的芯片制造商改變了策略,轉(zhuǎn)而在一個電路板上集成更多的處理器,也就是我們現(xiàn)在常見的多核處理器。
這就給軟件行業(yè)帶來麻煩(也可以說帶來機(jī)會,比如說就業(yè)機(jī)會,呵呵)。原來的情況是:我買一臺頻率比原來快一倍的處理器,那么我的程序就比原來快一倍,軟件工程師什么也不用干。現(xiàn)在不一樣了,我買一臺雙核的處理器,我的程序和原來一樣慢,當(dāng)然這條機(jī)器同時處理的任務(wù)可以變多了,但是對于單個任務(wù)來說并沒有幫助。
在幾年前,并發(fā)(Concurrent)和并行(Paralleling)程序設(shè)計(jì)還是在少量的地方使用,現(xiàn)在在個人的PC機(jī)上已經(jīng)是很常見了。(Concurrency and parallelism的區(qū)別參考 這個帖子)
造個諸葛亮的價錢遠(yuǎn)遠(yuǎn)高于造三個臭皮匠!多核是在一臺機(jī)器上的并發(fā),但是單機(jī)也是會到極限,所以分布式的計(jì)算也是類似的思路,用大量普通的機(jī)器協(xié)作完成一項(xiàng)任務(wù)。
但是要想編寫一個正確并且高效的能利用多核的多線程程序不是件容易的是,更別說分布式的情況(網(wǎng)絡(luò)問題,機(jī)器故障,負(fù)載均衡,。。。)?,F(xiàn)在的編譯器沒有辦法把單線程的程序自動編譯成一個多線程的版本(如果到了那一天,估計(jì)所有的程序員就失業(yè)了)。所以只能提供一些語言上的支持(比如scala/erlang)或者mapreduce這樣的框架。
Java雖然沒有提供scala那樣的基于消息的模型,但是也提供了豐富的concurrent特性,并且屏蔽了平臺的相關(guān)性(這不是件容易的事,比如多個處理器有自己的緩存,他們寫的東西不會離開被其它處理器看到),下面我們看看java的內(nèi)存模型(JMM)
JMM(Java Memory Model)
并行程序有很多模型,比如共享內(nèi)存模型,消息傳遞模型等等。這些模型或多或少的利用了平臺相關(guān)的特性(在并行程序設(shè)計(jì)里很難回避平臺的特性以便高效的通信),Java抽象出了自己的內(nèi)存模型,使得開放人員看不到平臺的差異(這不是件容易的事),不過即使這樣,和傳統(tǒng)程序不同,我們還是不能完全不了解一些體系架構(gòu)的細(xì)節(jié)問題,至少我們得了解一些。
在共享內(nèi)存的多處理器體系架構(gòu)里(我們現(xiàn)在用的服務(wù)器甚至筆記本都是),每個處理器都有自己的局部緩存并定期的使之與內(nèi)存同步。不同的處理器架構(gòu)保證了不同程度的緩存一致性(cache coherence),所以操作系統(tǒng),編譯器和運(yùn)行時環(huán)境必須一起努力來彌補(bǔ)平臺的差異性。
讓每個處理器都知道其它處理器的狀態(tài)的代價是非常昂貴的,所以大多數(shù)架構(gòu)都不會保證一致性,這通常不會有什么問題:進(jìn)程/線程直接并不共享信息,編譯器可以調(diào)整代碼執(zhí)行順序以便提高效率,我們都很開心。當(dāng)然也有需要在線程之間進(jìn)行同步的時候,比如某個線程要讀取到另一個線程寫入的信息,這個時候緩存里的數(shù)據(jù)就得同步到內(nèi)存里才行。所以這些體系架構(gòu)都提供了一些指令來完成數(shù)據(jù)的同步(當(dāng)然這些指令是非常費(fèi)時的,能不做就盡量不做)。這些指令一般叫做memory barriers or fences。當(dāng)然只是很底層的一些東西,所幸Java提供了一些高層的抽象,讓我們的生活變得容易一些。
sequential consistency: 我們假設(shè)一個線程執(zhí)行(可能在多個處理器上切換),每個變量讀取到的值都是最新的修改(也就是Cache里的立馬生效),這樣得到的結(jié)果是我們預(yù)期的。
但是讓我們意外的事情是:如果我們不做任何事情,那么很可能會出現(xiàn)錯誤,比如下面的這個例子:
public class NoVisibility { private static boolean ready; private static int number; private static class ReaderThread extends Thread { public void run() { while (!ready) Thread.yield(); System.out.println(number); } } public static void main(String[] args) { new ReaderThread().start(); number = 42; ready = true; } }
我們在主線程里先讓number=42(初始值是0),然后讓ready=true,而另一個線程不斷堅(jiān)持是否ready,如果ready,那么讀出number。很自然的我們期望子線程打印出42,但是很可能結(jié)果會另我們失望。編譯器可能會調(diào)換number=42 和 ready=true的順序(思考一下為什么它要這么干?為什么在單線程的情況下沒有問題?),另外子線程可能永遠(yuǎn)在while里死循環(huán)。為什么?子線程會永遠(yuǎn)看不到ready的變化?這也許讓很多人吃驚,事實(shí)確實(shí)如此,JSR并不保證這一點(diǎn)(雖然大多數(shù)時候子線程能夠退出),參考這個帖子和JMM的文章
vilatile和snychronized(intrinsic Lock)
vilatile關(guān)鍵字告訴編譯器,一個線程對某個變量的修改立即對所有其它線程看見,加上這個能保證上面的程序不會死循環(huán)。但是不能保證讀到42,也就是保證number=42和ready=true的執(zhí)行順序,要保證這點(diǎn)就要用到synchronized。
synchronized能夠保證執(zhí)行的順序,除此之外,它也能保證可見性。
public class NoVisibility { private static boolean ready; private static int number; private static class ReaderThread extends Thread { public void run() { boolean r=false; while (true){ synchronized(NoVisibility.class){ r=ready; } if(r) break; else Thread.yield(); } System.out.println(number); } } public static void main(String[] args) { new ReaderThread().start(); synchronized(NoVisibility.class){ number = 42; ready = true; } } }
synchronized(NoVisibility.class){ number = 42; ready = true; }
這段代碼保證了兩個語句的執(zhí)行順序
synchronized(NoVisibility.class){ r=ready; }
這保證子線程能看到ready的變化 注意他們必須synchronized同一個對象,如果是下面的代碼,則不能有任何保障。為什么?試想任何synchronized里的變量必須立即對所有的可見,那么代價太大, 比如我有這樣的需求:我只要求兩個語句順序執(zhí)行,它是否對別人可見我并不關(guān)心。
synchronized(AnotherObject){ r=ready; }
每個對象都有個Monitor,所以synchronized也經(jīng)常叫Monitor Lock,另外這個鎖是語言內(nèi)置的,所以也叫Intrinsic Lock。 這兩個關(guān)鍵字是java1.5之前就有了,在java1.5之后新引進(jìn)了java.util.concurrent包,這里有我們需要關(guān)注的很多東西,這里我們只關(guān)心Lock相關(guān)的接口和類。 不過synchronized來解決互斥不是很完美嗎?我為什么要花力氣搞這些新鮮東西呢?下面我們來看看synchronized解決不了(或者很難解決)的問題
銀行轉(zhuǎn)賬的例子
// Warning: deadlock-prone! public void transferMoney(Account fromAccount, Account toAccount, DollarAmount amount) throws InsufficientFundsException { synchronized (fromAccount) { synchronized (toAccount) { if (fromAccount.getBalance().compareTo(amount) < 0) throw new InsufficientFundsException(); else { fromAccount.debit(amount); toAccount.credit(amount); } } } }
比如我要在兩個用戶之間轉(zhuǎn)賬,為了防止意外,我必須同時鎖定兩個賬戶。但是這可能造成死鎖。比如:
A: transferMoney(myAccount, yourAccount, 10); B: transferMoney(yourAccount, myAccount, 20);
當(dāng)線程A鎖住myAccount時,B鎖住了toAccount,這個時候A嘗試鎖住toAccount,但是已經(jīng)被B鎖住,所以A不能繼續(xù)運(yùn)行,同理B也不能運(yùn)行,造成死鎖。
怎么解決呢?你也許回想,我先鎖住一個賬戶,然后"嘗試"鎖定另一個賬戶,如果“失敗”,那么我釋放所有的鎖,“休息”一下再繼續(xù)嘗試,當(dāng)然兩個線程節(jié)拍一致的話,可能造成“活鎖”
可惜synchronized不能提供這樣的語義,它一旦嘗試加鎖,只能拿到鎖,你不能控制它,比如你可能有這樣的需求:嘗試拿鎖30s,如果拿不到就算了,synchronized是沒辦法滿足這樣的需求的。另外你使用“鴕鳥”策略來解決死鎖:什么也不干,如果死鎖了,kill他們,重啟他們。這種策略看起來很瘋狂,不過如果死鎖的概率很多,而避免死鎖的算法很復(fù)雜,那這也是可以一試的策略(那一堆死鎖發(fā)生的充分必要條件太麻煩了?。。。?。下面我們仔細(xì)的來看看java1.5后提供的Lock接口及其相關(guān)類。
Lock接口
Lock的基本用法如下,為了防止異常退出時沒有釋放鎖,一般都在拿到鎖后立馬try,try住所有臨界區(qū)的代碼,然后finally釋放鎖。
主要和synchronized的區(qū)別,synchronized里我們不用操心這些,如果synchronized保護(hù)的代碼拋出異常,那么jvm會釋放掉Monitor Lock。
Lock l = ... l.lock(); try { // access the resource protected by this lock } finally { l.unlock(); }
Lock.lock()在鎖定成功后釋放鎖之前,它所保護(hù)的代碼段必須與使用synchronized保護(hù)的代碼段有相同的語義(可見性,順序性)。
所以從這個角度來說,Lock完全可以代替synchronized,那么是否應(yīng)該拋棄掉synchronized呢?答案是否定的。
是否應(yīng)該拋棄synchronized?
在java5引進(jìn)Lock后,實(shí)現(xiàn)了Lock接口的類就是ReentrantLock(呆會再解釋Reentrant),因?yàn)閖ava5之前synchronized的實(shí)現(xiàn)很爛,同樣是為了實(shí)現(xiàn)互斥,ReentrantLock會比synchronized速度上快很多,不過到了jdk6之后就不是這樣了,下面是一個測試結(jié)果: from book "Java Concurrency in Practice"
橫軸是線程數(shù),縱軸是ReentrantLock的吞吐量/IntrinsicLock的吞吐量。
可以看出,jdk5中,ReentrantLock快很多,但是到了jdk6,他們就沒什么大的差別了。
synchronized的優(yōu)點(diǎn):鎖的釋放是語言內(nèi)置的,不會出現(xiàn)忘記釋放鎖的情況,另外由于是語言內(nèi)置的支持,調(diào)試是能很快知道鎖被哪個線程持有,它加鎖的次數(shù)。而Lock只是util.concurrent一個普通的類,所以調(diào)試器并不知道這個鎖的任何信息,它只是一個普通的對象(當(dāng)然你可以仔細(xì)觀察每個線程的stack frame來看它在等待鎖)。
所以建議:如果只是為了實(shí)現(xiàn)互斥,那么使用synchronized(扔掉jdk5吧,現(xiàn)在都java7了),如果想用Lock附加的功能,那么才使用Lock。
下面回來繼續(xù)看Lock接口。
Interface Lock
public interface Lock { void lock(); void lockInterruptibly() throws InterruptedException; boolean tryLock(); boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException; void unlock(); Condition newCondition(); }
void lock();
嘗試獲取鎖。如果鎖被別人拿著,那么當(dāng)前線程不在執(zhí)行,也不能被調(diào)度,直到拿到鎖為止。
void lockInterruptibly() throws InterruptedException
嘗試獲取鎖,除非被interrupted。如果鎖可以獲取,那么立刻返回。
如果無非獲取鎖,那么線程停止執(zhí)行,并且不能被再調(diào)度,直到:
- 當(dāng)前線程獲得鎖
- 如果鎖的實(shí)現(xiàn)支持interruption,并且有其它線程interrupt當(dāng)前線程。
仔細(xì)閱讀javadoc的第二個情況:Lock接口并不要求Lock的實(shí)現(xiàn)支持interruption,不過sun jdk的實(shí)現(xiàn)都是支持的。
這個函數(shù)在下面兩個情況下拋出InterruptedException:
- 如果鎖的實(shí)現(xiàn)支持interruption,并且有其它線程interrupt當(dāng)前線程。
- 線程調(diào)用這個函數(shù)之前就被設(shè)置了interrupted狀態(tài)位
可以發(fā)現(xiàn)這個方法并不區(qū)分這個interrupted狀態(tài)位是之前就有的還是lock過程中產(chǎn)生的。不管如果,拋出異常后會清除interrupted標(biāo)記。
使用這個方法,我們可以中斷某個等鎖的線程,比如我們檢測到了死鎖,那么我們可以中斷這個線程
boolean tryLock()
嘗試獲取鎖,如果可以,那么鎖住對象然后返回true,否則返回false,不管怎么樣,這個方法會立即返回。下面的例子展示了用這個方法來解決前面轉(zhuǎn)賬的死鎖:
public boolean transferMoney(Account fromAcct, Account toAcct, DollarAmount amount, long timeout, TimeUnit unit) throws InsufficientFundsException, InterruptedException { long fixedDelay = getFixedDelayComponentNanos(timeout, unit); long randMod = getRandomDelayModulusNanos(timeout, unit); long stopTime = System.nanoTime() + unit.toNanos(timeout); while (true) { if (fromAcct.lock.tryLock()) { try { if (toAcct.lock.tryLock()) { try { if (fromAcct.getBalance().compareTo(amount) < 0) throw new InsufficientFundsException(); else { fromAcct.debit(amount); toAcct.credit(amount); return true; } } finally { toAcct.lock.unlock(); } } } finally { fromAcct.lock.unlock(); } } if (System.nanoTime() < stopTime) return false; NANOSECONDS.sleep(fixedDelay + rnd.nextLong() % randMod); } }
tryLock boolean tryLock(long time, TimeUnit unit) throws InterruptedException
和tryLock類似,不過不是立即返回,而是嘗試一定時間后還拿不到鎖就返回
unlock
釋放鎖
newCondition
暫且不管
Class ReentrantLock
這是sun jdk(open jdk)里唯一直接實(shí)現(xiàn)了Lock接口的類,所以如果你想用Lock的那些特性,比如tryLock,那么就應(yīng)該首先考慮它
首先我們解釋一下Reentrant
Reentrant翻譯成中文應(yīng)該是“可重入”,對于鎖來說,可重入是指如果一個線程已拿到過一把鎖,那么它可以再次拿到鎖。
聽起來似乎沒有什么意思,讓我們來看看“不可重入”鎖可能的一些問題和需要使用”可重入“鎖的場景吧。
public class Widget { public synchronized void doSomething() { ... } } public class LoggingWidget extends Widget { public synchronized void doSomething() { System.out.println(toString() + ": calling doSomething"); super.doSomething(); } } Widget widget=new LoggingWidget(); widget.doSomething();
設(shè)想這樣一個應(yīng)用場景:我們有一個圖的數(shù)據(jù)結(jié)構(gòu),我們需要遍歷所有節(jié)點(diǎn),找到滿足某些條件的節(jié)點(diǎn),鎖定所有這些節(jié)點(diǎn),然后對他們進(jìn)行一些操作。由于圖的遍歷可能重復(fù)訪問某個節(jié)點(diǎn),如果簡單的鎖定每個滿足條件的節(jié)點(diǎn),那么可能死鎖。當(dāng)然我們可以自己用程序記下哪些節(jié)點(diǎn)已經(jīng)訪問過了,不過也可以把這就事情交給ReentrantLock,第二次鎖定某個對象也會成功并立即返回。那么你可能會問,我釋放鎖的時候怎么記得它鎖定過了多少次呢?如果釋放少了,那么會死鎖;釋放多了,可能也會有問題(有些鎖實(shí)現(xiàn)會拋出異常,但是JMM好像沒有定義)。
【上面的場景參考http://stackoverflow.com/questions/1312259/what-is-the-re-entrant-lock-and-concept-in-general】 不用擔(dān)心,ReentrantLock提供了getHoldCount方法,最后釋放這么多次就好了。
ReentrantLock會記下當(dāng)前拿鎖的線程,已經(jīng)拿鎖的次數(shù),每次unlock都會減一,如果為零了,那么釋放鎖,另一個線程到鎖并且計(jì)數(shù)器值為一。
ReentrantLock的構(gòu)造函數(shù)可以接受一個fairness的參數(shù)。如果為true,那么它會傾向于把鎖給等待時間最長的線程。但是這樣的代價也是巨大的:
橫軸是并發(fā)線程數(shù),參考方法是ConcurrentHashMap,另外分別用Nonfair Lock和 fair Lock封裝普通的HashMap,可以看到,是否fair的差別是非常巨大的。
正如前面所說的,ReentrantLock是支持Interrupted的。
Interface ReadWriteLock
有的應(yīng)用場景下,有兩類角色:Reader和Writer。Reader讀取數(shù)據(jù),Writer更新數(shù)據(jù)。多個Reader同時讀取是沒有問題的,但是Reader們和Writer是互斥的,并且Writer和Writer也是互斥的。而且很多應(yīng)用中,Reader會很多,而Writer會比較少。這個接口就是為了解決這類特殊場景的。
public interface ReadWriteLock { Lock readLock(); Lock writeLock(); } 用法: ReadWriteLock rwl = ...; //Reader threads read(){ rwl.readLock().lock(); try{ //entering critical setion }finally{ rwl.readLock().unlock(); } } write(){ rwl.writeLock().lock(); try{ //entering critical setion }finally{ rwl.writeLock().unlock(); }}
Class ReentrantReadWriteLock
這是Sun jdk里唯一實(shí)現(xiàn)ReadWriteLock接口的類。 這個類的特性:
獲取鎖的順序
這個類并不傾向Reader或者Writer,不過有個fairness的策略 非公平模式(默認(rèn))
如果很多Reader和Writer的話,很可能Reder一直能獲取鎖,而Writer可能會饑餓
公平模式
這種模式下,會盡量以請求鎖的順序來保證公平性。當(dāng)前鎖釋放以后,等待時間最長的Writer或者一組Reader(Reader是一伙的?。┇@取鎖。 如果鎖被拿著,這時Writer來了,他會開始排隊(duì);如果Reader來了,如果它之前沒有Writer并且當(dāng)前拿鎖的是Reader,那么它直接就拿到鎖,當(dāng)然如果是Writer拿著,那么它也只能排 隊(duì)等鎖。 不過如果Reader拿著鎖,Writer排隊(duì),然后Reader排在Writer后,但是Writer放棄了排隊(duì)(比如它用的是tryLock 30s),那么Reader直接拿到鎖而不用排隊(duì)。
還有就是ReentrantReadWriteLock.ReadLock.tryLock() 和 ReentrantReadWriteLock.WriteLock.tryLock()方法不管這些,一旦調(diào)用的時候能拿到鎖,那么它們就會插隊(duì)!!
Reentrancy
從名字就知道它支持可重入。
以前拿過鎖的Reader和Writer可以繼續(xù)拿鎖。另外拿到WriteLock的線程可以拿到ReadLock,但是反之不然。
Lock downgrading
拿到WriteLock的可以直接變成ReadLock,不用釋放WriteLock再從新請求ReadLock(這樣需要重新排隊(duì)),實(shí)現(xiàn)的方法是先拿到WriteLock,接著拿ReadLock(上面的特性保證了不會死鎖),然后釋放WriteLock,這樣就得到一個ReadLock并立馬持有。
Interruption of lock acquisition
支持
一個使用讀寫鎖的例子
class CachedData { Object data; volatile boolean cacheValid; ReentrantReadWriteLock rwl = new ReentrantReadWriteLock(); void processCachedData() { rwl.readLock().lock(); if (!cacheValid) { // Must release read lock before acquiring write lock rwl.readLock().unlock(); rwl.writeLock().lock(); // Recheck state because another thread might have acquired // write lock and changed state before we did. if (!cacheValid) { data = ... cacheValid = true; } // Downgrade by acquiring read lock before releasing write lock rwl.readLock().lock(); rwl.writeLock().unlock(); // Unlock write, still hold read } use(data); rwl.readLock().unlock(); } }
一個Cache數(shù)據(jù)的例子,讀取數(shù)據(jù)時首先拿讀鎖,如果cache是有效的(volatile boolean cacheValid),直接使用數(shù)據(jù)。
如果失效了,那么釋放讀鎖,獲取寫鎖【這個類不支持upgrading】,然后double check一下是否cache有效,如果還是無效(說明它應(yīng)該更新),那么更新數(shù)據(jù),并且修改變量cacheValid,讓其它線程看到。
臭名昭著的double check
前面提到了double check,這里也順便討論一下:
@NotThreadSafe public class DoubleCheckedLocking { private static Resource resource; public static Resource getInstance() { if (resource == null) { synchronized (DoubleCheckedLocking.class) { if (resource == null) resource = new Resource(); } } return resource; } }
很多“hacker”再提到延遲加載的時候都會提到它,上面的代碼看起來沒有什么問題:首先檢查一些resource,如果為空,那么加鎖,因?yàn)闄z查resource==null沒有加鎖,所以可能同時兩個線程進(jìn)入if并且請求加鎖,所以第一個拿到鎖的初始化一次,第二次拿鎖的會再次check。這看起來很完美:大多數(shù)情況下resouce不為空,很少的情況(剛開始時)resource為空,那么再加鎖,這比一上來就加鎖要高效很多不過千萬別高興地太早了,因?yàn)榫幾g器對引用的賦值可能會做優(yōu)化,可能這個對象還沒有正確的構(gòu)造好,值已經(jīng)賦好了(為什么要這么做?也許構(gòu)造對象需要IO,io等待的時間把值賦好了能提高速度)。這個時候別的線程就慘了!另外很多講延遲加載的文章都比較早(早于jdk6),那個年代java的synchronized確實(shí)很不給力。如果你實(shí)在在乎這點(diǎn)性能的話,應(yīng)該用jvm的靜態(tài)類加載機(jī)制來實(shí)現(xiàn):
@ThreadSafe public class ResourceFactory { private static class ResourceHolder { public static Resource resource = new Resource(); } public static Resource getResource() { return ResourceHolder.resource ; } }
到此這篇關(guān)于Java多線程中Lock鎖的使用總結(jié)的文章就介紹到這了,更多相關(guān)Java多線程 Lock鎖的使用內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
json序列化時忽略值為null的字段2種方式實(shí)例
這篇文章主要給大家介紹了關(guān)于json序列化時忽略值為null的字段的2種方式,當(dāng)對象中某個字段為null時,我們希望將對象轉(zhuǎn)換成json時為null的字段不會被轉(zhuǎn)換到j(luò)son字符串,里面需要的朋友可以參考下2023-10-10Java postgresql數(shù)組字段類型處理方法詳解
這篇文章主要介紹了Java postgresql數(shù)組字段類型處理方法,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-10-10解決nacos修改配置信息后需要重啟服務(wù)才能生效的問題
當(dāng)配置信息發(fā)生變動時,傳統(tǒng)修改配置信息后,需要重新重啟服務(wù)器才可以生效,大量應(yīng)用配置修改時,需要一個個修改配置,無法統(tǒng)一修改,且沒有辦法回溯配置版本,所以本文給大家介紹了如何解決這些問題的方法,需要的朋友可以參考下2023-10-10Java OpenSSL生成的RSA公私鑰進(jìn)行數(shù)據(jù)加解密詳細(xì)介紹
這篇文章主要介紹了Java OpenSSL生成的RSA公私鑰進(jìn)行數(shù)據(jù)加解密詳細(xì)介紹的相關(guān)資料,這里提供實(shí)例代碼及說明具體如何實(shí)現(xiàn),需要的朋友可以參考下2016-12-12Springboot實(shí)現(xiàn)全局自定義異常的方法詳解
這篇文章主要介紹了Springboot實(shí)現(xiàn)全局自定義異常的方法詳解,SpringBoot的項(xiàng)目已經(jīng)對有一定的異常處理了,但是對于我們開發(fā)者而言可能就不太合適了,因此我們需要對這些異常進(jìn)行統(tǒng)一的捕獲并處理,需要的朋友可以參考下2023-11-11