聊聊并發(fā)編程的10個(gè)坑
前言
對(duì)于從事后端開發(fā)的同學(xué)來(lái)說(shuō),并發(fā)編程肯定再熟悉不過(guò)了。
說(shuō)實(shí)話,在java中并發(fā)編程是一大難點(diǎn),至少我是這么認(rèn)為的。不光理解起來(lái)比較費(fèi)勁,使用起來(lái)更容易踩坑。
不信,讓繼續(xù)往下面看。
今天重點(diǎn)跟大家一起聊聊并發(fā)編程的10個(gè)坑,希望對(duì)你有幫助。

1. SimpleDateFormat線程不安全
在java8之前,我們對(duì)時(shí)間的格式化處理,一般都是用的SimpleDateFormat類實(shí)現(xiàn)的。例如:
@Service
public?class?SimpleDateFormatService?{
????public?Date?time(String?time)?throws?ParseException?{
????????SimpleDateFormat?dateFormat?=?new?SimpleDateFormat("yyyy-MM-dd?HH:mm:ss");
????????return?dateFormat.parse(time);
????}
}
如果你真的這樣寫,是沒(méi)問(wèn)題的。
就怕哪天抽風(fēng),你覺(jué)得dateFormat是一段固定的代碼,應(yīng)該要把它抽取成常量。
于是把代碼改成下面的這樣:
@Service
public?class?SimpleDateFormatService?{
???private?static?SimpleDateFormat?dateFormat?=?new?SimpleDateFormat("yyyy-MM-dd?HH:mm:ss");
????public?Date?time(String?time)?throws?ParseException?{
????????return?dateFormat.parse(time);
????}
}
dateFormat對(duì)象被定義成了靜態(tài)常量,這樣就能被所有對(duì)象共用。
如果只有一個(gè)線程調(diào)用time方法,也不會(huì)出現(xiàn)問(wèn)題。
但Serivce類的方法,往往是被Controller類調(diào)用的,而Controller類的接口方法,則會(huì)被tomcat的線程池調(diào)用。換句話說(shuō),可能會(huì)出現(xiàn)多個(gè)線程調(diào)用同一個(gè)Controller類的同一個(gè)方法,也就是會(huì)出現(xiàn)多個(gè)線程會(huì)同時(shí)調(diào)用time方法的情況。
而time方法會(huì)調(diào)用SimpleDateFormat類的parse方法:
@Override
public?Date?parse(String?text,?ParsePosition?pos)?{
????...
????Date?parsedDate;
????try?{
????????parsedDate?=?calb.establish(calendar).getTime();
????????...
????}?catch?(IllegalArgumentException?e)?{
????????pos.errorIndex?=?start;
????????pos.index?=?oldStart;
????????return?null;
????}
???return?parsedDate;
}?
該方法會(huì)調(diào)用establish方法:
Calendar?establish(Calendar?cal)?{
????...
????//1.清空數(shù)據(jù)
????cal.clear();
????//2.設(shè)置時(shí)間
????cal.set(...);
????//3.返回
????return?cal;
}
其中的步驟1、2、3是非原子操作。
但如果cal對(duì)象是局部變量還好,壞就壞在parse方法調(diào)用establish方法時(shí),傳入的calendar是SimpleDateFormat類的父類DateFormat的成員變量:
public?abstract?class?DateFormat?extends?Forma?{
????....
????protected?Calendar?calendar;
????...
}
這樣就可能會(huì)出現(xiàn)多個(gè)線程,同時(shí)修改同一個(gè)對(duì)象即:dateFormat,他的同一個(gè)成員變量即:Calendar值的情況。
這樣可能會(huì)出現(xiàn),某個(gè)線程設(shè)置好了時(shí)間,又被其他的線程修改了,從而出現(xiàn)時(shí)間錯(cuò)誤的情況。
那么,如何解決這個(gè)問(wèn)題呢?
SimpleDateFormat類的對(duì)象不要定義成靜態(tài)的,可以改成方法的局部變量。 使用ThreadLocal保存SimpleDateFormat類的數(shù)據(jù)。 使用java8的DateTimeFormatter類。
2. 雙重檢查鎖的漏洞
單例模式無(wú)論在實(shí)際工作,還是在面試中,都出現(xiàn)得比較多。
我們都知道,單例模式有:餓漢模式和懶漢模式兩種。
餓漢模式代碼如下:
public?class?SimpleSingleton?{
????//持有自己類的引用
????private?static?final?SimpleSingleton?INSTANCE?=?new?SimpleSingleton();
????//私有的構(gòu)造方法
????private?SimpleSingleton()?{
????}
????//對(duì)外提供獲取實(shí)例的靜態(tài)方法
????public?static?SimpleSingleton?getInstance()?{
????????return?INSTANCE;
????}
}
使用餓漢模式的好處是:沒(méi)有線程安全的問(wèn)題,但帶來(lái)的壞處也很明顯。
private?static?final?SimpleSingleton?INSTANCE?=?new?SimpleSingleton();
一開始就實(shí)例化對(duì)象了,如果實(shí)例化過(guò)程非常耗時(shí),并且最后這個(gè)對(duì)象沒(méi)有被使用,不是白白造成資源浪費(fèi)嗎?
還真是啊。
這個(gè)時(shí)候你也許會(huì)想到,不用提前實(shí)例化對(duì)象,在真正使用的時(shí)候再實(shí)例化不就可以了?
這就是我接下來(lái)要介紹的:懶漢模式。
具體代碼如下:
public?class?SimpleSingleton2?{
????private?static?SimpleSingleton2?INSTANCE;
????private?SimpleSingleton2()?{
????}
????public?static?SimpleSingleton2?getInstance()?{
????????if?(INSTANCE?==?null)?{
????????????INSTANCE?=?new?SimpleSingleton2();
????????}
????????return?INSTANCE;
????}
}
示例中的INSTANCE對(duì)象一開始是空的,在調(diào)用getInstance方法才會(huì)真正實(shí)例化。
嗯,不錯(cuò)不錯(cuò)。但這段代碼還是有問(wèn)題。
假如有多個(gè)線程中都調(diào)用了getInstance方法,那么都走到 if (INSTANCE == null) 判斷時(shí),可能同時(shí)成立,因?yàn)镮NSTANCE初始化時(shí)默認(rèn)值是null。這樣會(huì)導(dǎo)致多個(gè)線程中同時(shí)創(chuàng)建INSTANCE對(duì)象,即INSTANCE對(duì)象被創(chuàng)建了多次,違背了只創(chuàng)建一個(gè)INSTANCE對(duì)象的初衷。
為了解決餓漢模式和懶漢模式各自的問(wèn)題,于是出現(xiàn)了:雙重檢查鎖。
具體代碼如下:
public?class?SimpleSingleton4?{
????private?static?SimpleSingleton4?INSTANCE;
????private?SimpleSingleton4()?{
????}
????public?static?SimpleSingleton4?getInstance()?{
????????if?(INSTANCE?==?null)?{
????????????synchronized?(SimpleSingleton4.class)?{
????????????????if?(INSTANCE?==?null)?{
????????????????????INSTANCE?=?new?SimpleSingleton4();
????????????????}
????????????}
????????}
????????return?INSTANCE;
????}
}
需要在synchronized前后兩次判空。
但我要告訴你的是:這段代碼有漏洞的。
有什么問(wèn)題?
public?static?SimpleSingleton4?getInstance()?{
????if?(INSTANCE?==?null)?{//1
????????synchronized?(SimpleSingleton4.class)?{//2
????????????if?(INSTANCE?==?null)?{//3
????????????????INSTANCE?=?new?SimpleSingleton4();//4
????????????}
????????}
????}
????return?INSTANCE;//5
}
getInstance方法的這段代碼,我是按1、2、3、4、5這種順序?qū)懙模M舶催@個(gè)順序執(zhí)行。
但是java虛擬機(jī)實(shí)際上會(huì)做一些優(yōu)化,對(duì)一些代碼指令進(jìn)行重排。重排之后的順序可能就變成了:1、3、2、4、5,這樣在多線程的情況下同樣會(huì)創(chuàng)建多次實(shí)例。重排之后的代碼可能如下:
public?static?SimpleSingleton4?getInstance()?{
????if?(INSTANCE?==?null)?{//1
???????if?(INSTANCE?==?null)?{//3
???????????synchronized?(SimpleSingleton4.class)?{//2
????????????????INSTANCE?=?new?SimpleSingleton4();//4
????????????}
????????}
????}
????return?INSTANCE;//5
}
原來(lái)如此,那有什么辦法可以解決呢?
答:可以在定義INSTANCE是加上volatile關(guān)鍵字。具體代碼如下:
public?class?SimpleSingleton7?{
????private?volatile?static?SimpleSingleton7?INSTANCE;
????private?SimpleSingleton7()?{
????}
????public?static?SimpleSingleton7?getInstance()?{
????????if?(INSTANCE?==?null)?{
????????????synchronized?(SimpleSingleton7.class)?{
????????????????if?(INSTANCE?==?null)?{
????????????????????INSTANCE?=?new?SimpleSingleton7();
????????????????}
????????????}
????????}
????????return?INSTANCE;
????}
}
volatile關(guān)鍵字可以保證多個(gè)線程的可見(jiàn)性,但是不能保證原子性。同時(shí)它也能禁止指令重排。
雙重檢查鎖的機(jī)制既保證了線程安全,又比直接上鎖提高了執(zhí)行效率,還節(jié)省了內(nèi)存空間。
3. volatile的原子性
從前面我們已經(jīng)知道volatile,是一個(gè)非常不錯(cuò)的關(guān)鍵字,它能保證變量在多個(gè)線程中的可見(jiàn)性,它也能禁止指令重排,但是不能保證原子性。
使用volatile關(guān)鍵字禁止指令重排,前面已經(jīng)說(shuō)過(guò)了,這里就不聊了。
可見(jiàn)性主要體現(xiàn)在:一個(gè)線程對(duì)某個(gè)變量修改了,另一個(gè)線程每次都能獲取到該變量的最新值。
先一起看看反例:
public?class?VolatileTest?extends?Thread?{
????private??boolean?stopFlag?=?false;
????public?boolean?isStopFlag()?{
????????return?stopFlag;
????}
????@Override
????public?void?run()?{
????????try?{
????????????Thread.sleep(300);
????????}?catch?(InterruptedException?e)?{
????????????e.printStackTrace();
????????}
????????stopFlag?=?true;
????????System.out.println(Thread.currentThread().getName()?+?"?stopFlag?=?"?+?stopFlag);
????}
????public?static?void?main(String[]?args)?{
????????VolatileTest?vt?=?new?VolatileTest();
????????vt.start();
????????while?(true)?{
????????????if?(vt.isStopFlag())?{
????????????????System.out.println("stop");
????????????????break;
????????????}
????????}
????}
}
上面這段代碼中,VolatileTest是一個(gè)Thread類的子類,它的成員變量stopFlag默認(rèn)是false,在它的run方法中修改成了true。
然后在main方法的主線程中,用vt.isStopFlag()方法判斷,如果它的值是true時(shí),則打印stop關(guān)鍵字。
那么,如何才能讓stopFlag的值修改了,在主線程中通過(guò)vt.isStopFlag()方法,能夠獲取最新的值呢?
正例如下:
public?class?VolatileTest?extends?Thread?{
????private?volatile?boolean?stopFlag?=?false;
????public?boolean?isStopFlag()?{
????????return?stopFlag;
????}
????@Override
????public?void?run()?{
????????try?{
????????????Thread.sleep(300);
????????}?catch?(InterruptedException?e)?{
????????????e.printStackTrace();
????????}
????????stopFlag?=?true;
????????System.out.println(Thread.currentThread().getName()?+?"?stopFlag?=?"?+?stopFlag);
????}
????public?static?void?main(String[]?args)?{
????????VolatileTest?vt?=?new?VolatileTest();
????????vt.start();
????????while?(true)?{
????????????if?(vt.isStopFlag())?{
????????????????System.out.println("stop");
????????????????break;
????????????}
????????}
????}
}
用volatile關(guān)鍵字修飾stopFlag即可。
下面重點(diǎn)說(shuō)說(shuō)volatile的原子性問(wèn)題。
使用多線程給count加1,代碼如下:
public?class?VolatileTest?{
????public?volatile?int?count?=?0;
????public?void?add()?{
????????count++;
????}
????public?static?void?main(String[]?args)?{
????????final?VolatileTest?test?=?new?VolatileTest();
????????for?(int?i?=?0;?i?20;?i++)?{
????????????new?Thread()?{
????????????????@Override
????????????????public?void?run()?{
????????????????????for?(int?j?=?0;?j?1000;?j++)?{
????????????????????????test.add();
????????????????????}
????????????????}
????????????????;
????????????}.start();
????????}
????????while?(Thread.activeCount()?>?2)?{
????????????//保證前面的線程都執(zhí)行完
????????????Thread.yield();
????????}
????????System.out.println(test.count);
????}
}
執(zhí)行結(jié)果每次都不一樣,但可以肯定的是count值每次都小于20000,比如:19999。
這個(gè)例子中count是成員變量,雖說(shuō)被定義成了volatile的,但由于add方法中的count++是非原子操作。在多線程環(huán)境中,count++的數(shù)據(jù)可能會(huì)出現(xiàn)問(wèn)題。
由此可見(jiàn),volatile不能保證原子性。
那么,如何解決這個(gè)問(wèn)題呢?
答:使用synchronized關(guān)鍵字。
改造后的代碼如下:
public?class?VolatileTest?{
????public?int?count?=?0;
????public?synchronized?void?add()?{
????????count++;
????}
????public?static?void?main(String[]?args)?{
????????final?VolatileTest?test?=?new?VolatileTest();
????????for?(int?i?=?0;?i?20;?i++)?{
????????????new?Thread()?{
????????????????@Override
????????????????public?void?run()?{
????????????????????for?(int?j?=?0;?j?1000;?j++)?{
????????????????????????test.add();
????????????????????}
????????????????}
????????????????;
????????????}.start();
????????}
????????while?(Thread.activeCount()?>?2)?{
????????????//保證前面的線程都執(zhí)行完
????????????Thread.yield();
????????}
????????System.out.println(test.count);
????}
}
4. 死鎖
死鎖可能是大家都不希望遇到的問(wèn)題,因?yàn)橐坏┏绦虺霈F(xiàn)了死鎖,如果沒(méi)有外力的作用,程序?qū)?huì)一直處于資源競(jìng)爭(zhēng)的假死狀態(tài)中。
死鎖代碼如下:
public?class?DeadLockTest?{
????public?static?String?OBJECT_1?=?"OBJECT_1";
????public?static?String?OBJECT_2?=?"OBJECT_2";
????public?static?void?main(String[]?args)?{
????????LockA?lockA?=?new?LockA();
????????new?Thread(lockA).start();
????????LockB?lockB?=?new?LockB();
????????new?Thread(lockB).start();
????}
}
class?LockA?implements?Runnable?{
????@Override
????public?void?run()?{
????????synchronized?(DeadLockTest.OBJECT_1)?{
????????????try?{
????????????????Thread.sleep(500);
????????????????synchronized?(DeadLockTest.OBJECT_2)?{
????????????????????System.out.println("LockA");
????????????????}
????????????}?catch?(InterruptedException?e)?{
????????????????e.printStackTrace();
????????????}
????????}
????}
}
class?LockB?implements?Runnable?{
????@Override
????public?void?run()?{
????????synchronized?(DeadLockTest.OBJECT_2)?{
????????????try?{
????????????????Thread.sleep(500);
????????????????synchronized?(DeadLockTest.OBJECT_1)?{
????????????????????System.out.println("LockB");
????????????????}
????????????}?catch?(InterruptedException?e)?{
????????????????e.printStackTrace();
????????????}
????????}
????}
}
一個(gè)線程在獲取OBJECT_1鎖時(shí),沒(méi)有釋放鎖,又去申請(qǐng)OBJECT_2鎖。而剛好此時(shí),另一個(gè)線程獲取到了OBJECT_2鎖,也沒(méi)有釋放鎖,去申請(qǐng)OBJECT_1鎖。由于OBJECT_1和OBJECT_2鎖都沒(méi)有釋放,兩個(gè)線程將一起請(qǐng)求下去,陷入死循環(huán),即出現(xiàn)死鎖的情況。
那么如果避免死鎖問(wèn)題呢?
4.1 縮小鎖的范圍
出現(xiàn)死鎖的情況,有可能是像上面那樣,鎖范圍太大了導(dǎo)致的。
那么解決辦法就是縮小鎖的范圍。
具體代碼如下:
class?LockA?implements?Runnable?{
????@Override
????public?void?run()?{
????????synchronized?(DeadLockTest.OBJECT_1)?{
????????????try?{
????????????????Thread.sleep(500);
????????????}?catch?(InterruptedException?e)?{
????????????????e.printStackTrace();
????????????}
????????}
????????synchronized?(DeadLockTest.OBJECT_2)?{
?????????????System.out.println("LockA");
????????}
????}
}
class?LockB?implements?Runnable?{
????@Override
????public?void?run()?{
????????synchronized?(DeadLockTest.OBJECT_2)?{
????????????try?{
????????????????Thread.sleep(500);
????????????}?catch?(InterruptedException?e)?{
????????????????e.printStackTrace();
????????????}
????????}
????????synchronized?(DeadLockTest.OBJECT_1)?{
?????????????System.out.println("LockB");
????????}
????}
}
在獲取OBJECT_1鎖的代碼塊中,不包含獲取OBJECT_2鎖的代碼。同時(shí)在獲取OBJECT_2鎖的代碼塊中,也不包含獲取OBJECT_1鎖的代碼。
4.2 保證鎖的順序
出現(xiàn)死鎖的情況說(shuō)白了是,一個(gè)線程獲取鎖的順序是:OBJECT_1和OBJECT_2。而另一個(gè)線程獲取鎖的順序剛好相反為:OBJECT_2和OBJECT_1。
那么,如果我們能保證每次獲取鎖的順序都相同,就不會(huì)出現(xiàn)死鎖問(wèn)題。
具體代碼如下:
class?LockA?implements?Runnable?{
????@Override
????public?void?run()?{
????????synchronized?(DeadLockTest.OBJECT_1)?{
????????????try?{
????????????????Thread.sleep(500);
????????????????synchronized?(DeadLockTest.OBJECT_2)?{
????????????????????System.out.println("LockA");
????????????????}
????????????}?catch?(InterruptedException?e)?{
????????????????e.printStackTrace();
????????????}
????????}
????}
}
class?LockB?implements?Runnable?{
????@Override
????public?void?run()?{
????????synchronized?(DeadLockTest.OBJECT_1)?{
????????????try?{
????????????????Thread.sleep(500);
????????????????synchronized?(DeadLockTest.OBJECT_2)?{
????????????????????System.out.println("LockB");
????????????????}
????????????}?catch?(InterruptedException?e)?{
????????????????e.printStackTrace();
????????????}
????????}
????}
}
兩個(gè)線程,每個(gè)線程都是先獲取OBJECT_1鎖,再獲取OBJECT_2鎖。
5. 沒(méi)釋放鎖
在java中除了使用synchronized關(guān)鍵字,給我們所需要的代碼塊加鎖之外,還能通過(guò)Lock關(guān)鍵字加鎖。
使用synchronized關(guān)鍵字加鎖后,如果程序執(zhí)行完畢,或者程序出現(xiàn)異常時(shí),會(huì)自動(dòng)釋放鎖。
但如果使用Lock關(guān)鍵字加鎖后,需要開發(fā)人員在代碼中手動(dòng)釋放鎖。
例如:
public?class?LockTest?{
????private?final?ReentrantLock?rLock?=?new?ReentrantLock();
????public?void?fun()?{
????????rLock.lock();
????????try?{
????????????System.out.println("fun");
????????}?finally?{
????????????rLock.unlock();
????????}
????}
}
代碼中先創(chuàng)建一個(gè)ReentrantLock類的實(shí)例對(duì)象rLock,調(diào)用它的lock方法加鎖。然后執(zhí)行業(yè)務(wù)代碼,最后再finally代碼塊中調(diào)用unlock方法。
但如果你沒(méi)有在finally代碼塊中,調(diào)用unlock方法手動(dòng)釋放鎖,線程持有的鎖將不會(huì)得到釋放。
6. HashMap導(dǎo)致內(nèi)存溢出
HashMap在實(shí)際的工作場(chǎng)景中,使用頻率還是挺高的,比如:接收參數(shù),緩存數(shù)據(jù),匯總數(shù)據(jù)等等。
但如果你在多線程的環(huán)境中使用HashMap,可能會(huì)導(dǎo)致非常嚴(yán)重的后果。
@Service
public?class?HashMapService?{
????private?Map?hashMap?=?new?HashMap<>();
????public?void?add(User?user)?{
????????hashMap.put(user.getId(),?user.getName());
????}
}
在HashMapService類中定義了一個(gè)HashMap的成員變量,在add方法中往HashMap中添加數(shù)據(jù)。在controller層的接口中調(diào)用add方法,會(huì)使用tomcat的線程池去處理請(qǐng)求,就相當(dāng)于在多線程的場(chǎng)景下調(diào)用add方法。
在jdk1.7中,HashMap使用的數(shù)據(jù)結(jié)構(gòu)是:數(shù)組+鏈表。如果在多線程的情況下,不斷往HashMap中添加數(shù)據(jù),它會(huì)調(diào)用resize方法進(jìn)行擴(kuò)容。該方法在復(fù)制元素到新數(shù)組時(shí),采用的頭插法,在某些情況下,會(huì)導(dǎo)致鏈表會(huì)出現(xiàn)死循環(huán)。
死循環(huán)最終結(jié)果會(huì)導(dǎo)致:內(nèi)存溢出。
此外,如果HashMap中數(shù)據(jù)非常多,會(huì)導(dǎo)致鏈表很長(zhǎng)。當(dāng)查找某個(gè)元素時(shí),需要遍歷某個(gè)鏈表,查詢效率不太高。
為此,jdk1.8之后,將HashMap的數(shù)據(jù)結(jié)構(gòu)改成了:數(shù)組+鏈表+紅黑樹。
如果同一個(gè)數(shù)組元素中的數(shù)據(jù)項(xiàng)小于8個(gè),則還是用鏈表保存數(shù)據(jù)。如果大于8個(gè),則自動(dòng)轉(zhuǎn)換成紅黑樹。
為什么要用紅黑樹?
答:鏈表的時(shí)間復(fù)雜度是O(n),而紅黑樹的時(shí)間復(fù)雜度是O(logn),紅黑樹的復(fù)雜度是優(yōu)于鏈表的。
既然這樣,為什么不直接使用紅黑樹?
答:樹節(jié)點(diǎn)所占存儲(chǔ)空間是鏈表節(jié)點(diǎn)的兩倍,節(jié)點(diǎn)少的時(shí)候,盡管在時(shí)間復(fù)雜度上,紅黑樹比鏈表稍微好一些。但是由于紅黑樹所占空間比較大,HashMap綜合考慮之后,認(rèn)為節(jié)點(diǎn)數(shù)量少的時(shí)候用占存儲(chǔ)空間更多的紅黑樹不劃算。
jdk1.8中HashMap就不會(huì)出現(xiàn)死循環(huán)?
答:錯(cuò),它在多線程環(huán)境中依然會(huì)出現(xiàn)死循環(huán)。在擴(kuò)容的過(guò)程中,在鏈表轉(zhuǎn)換為樹的時(shí)候,for循環(huán)一直無(wú)法跳出,從而導(dǎo)致死循環(huán)。
那么,如果想多線程環(huán)境中使用HashMap該怎么辦呢?
答:使用ConcurrentHashMap。
7. 使用默認(rèn)線程池
我們都知道jdk1.5之后,提供了ThreadPoolExecutor類,用它可以自定義線程池。
線程池的好處有很多,比如:
降低資源消耗:避免了頻繁的創(chuàng)建線程和銷毀線程,可以直接復(fù)用已有線程。而我們都知道,創(chuàng)建線程是非常耗時(shí)的操作。提供速度:任務(wù)過(guò)來(lái)之后,因?yàn)榫€程已存在,可以拿來(lái)直接使用。提高線程的可管理性:線程是非常寶貴的資源,如果創(chuàng)建過(guò)多的線程,不僅會(huì)消耗系統(tǒng)資源,甚至?xí)绊懴到y(tǒng)的穩(wěn)定。使用線程池,可以非常方便的創(chuàng)建、管理和監(jiān)控線程。
當(dāng)然jdk為了我們使用更便捷,專門提供了:Executors類,給我們快速創(chuàng)建線程池。
該類中包含了很多靜態(tài)方法:
newCachedThreadPool:創(chuàng)建一個(gè)可緩沖的線程,如果線程池大小超過(guò)處理需要,可靈活回收空閑線程,若無(wú)可回收,則新建線程。newFixedThreadPool:創(chuàng)建一個(gè)固定大小的線程池,如果任務(wù)數(shù)量超過(guò)線程池大小,則將多余的任務(wù)放到隊(duì)列中。newScheduledThreadPool:創(chuàng)建一個(gè)固定大小,并且能執(zhí)行定時(shí)周期任務(wù)的線程池。newSingleThreadExecutor:創(chuàng)建只有一個(gè)線程的線程池,保證所有的任務(wù)安裝順序執(zhí)行。
在高并發(fā)的場(chǎng)景下,如果大家使用這些靜態(tài)方法創(chuàng)建線程池,會(huì)有一些問(wèn)題。
那么,我們一起看看有哪些問(wèn)題?
newFixedThreadPool:允許請(qǐng)求的隊(duì)列長(zhǎng)度是Integer.MAX_VALUE,可能會(huì)堆積大量的請(qǐng)求,從而導(dǎo)致OOM。newSingleThreadExecutor:允許請(qǐng)求的隊(duì)列長(zhǎng)度是Integer.MAX_VALUE,可能會(huì)堆積大量的請(qǐng)求,從而導(dǎo)致OOM。newCachedThreadPool:允許創(chuàng)建的線程數(shù)是Integer.MAX_VALUE,可能會(huì)創(chuàng)建大量的線程,從而導(dǎo)致OOM。
那我們?cè)撛蹀k呢?
優(yōu)先推薦使用ThreadPoolExecutor類,我們自定義線程池。
具體代碼如下:
ExecutorService?threadPool?=?new?ThreadPoolExecutor(
????8,?//corePoolSize線程池中核心線程數(shù)
????10,?//maximumPoolSize?線程池中最大線程數(shù)
????60,?//線程池中線程的最大空閑時(shí)間,超過(guò)這個(gè)時(shí)間空閑線程將被回收
????TimeUnit.SECONDS,//時(shí)間單位
????new?ArrayBlockingQueue(500),?//隊(duì)列
????new?ThreadPoolExecutor.CallerRunsPolicy());?//拒絕策略
順便說(shuō)一下,如果是一些低并發(fā)場(chǎng)景,使用Executors類創(chuàng)建線程池也未嘗不可,也不能完全一棍子打死。在這些低并發(fā)場(chǎng)景下,很難出現(xiàn)OOM問(wèn)題,所以我們需要根據(jù)實(shí)際業(yè)務(wù)場(chǎng)景選擇。
8. @Async注解的陷阱
之前在java并發(fā)編程中實(shí)現(xiàn)異步功能,一般是需要使用線程或者線程池。
線程池的底層也是用的線程。
而實(shí)現(xiàn)一個(gè)線程,要么繼承Thread類,要么實(shí)現(xiàn)Runnable接口,然后在run方法中寫具體的業(yè)務(wù)邏輯代碼。
開發(fā)spring的大神們,為了簡(jiǎn)化這類異步操作,已經(jīng)幫我們把異步功能封裝好了。spring中提供了@Async注解,我們可以通過(guò)它即可開啟異步功能,使用起來(lái)非常方便。
具體做法如下:
1.在springboot的啟動(dòng)類上面加上@EnableAsync注解。
@EnableAsync
@SpringBootApplication
public?class?Application?{
????public?static?void?main(String[]?args)?{
????????SpringApplication.run(Application.class,?args);
????}
}
2.在需要執(zhí)行異步調(diào)用的業(yè)務(wù)方法加上@Async注解。
@Service
public?class?CategoryService?{
?????@Async
?????public?void?add(Category?category)?{
????????//添加分類
?????}
}
3.在controller方法中調(diào)用這個(gè)業(yè)務(wù)方法。
@RestController
@RequestMapping("/category")
public?class?CategoryController?{
?????@Autowired
?????private?CategoryService?categoryService;
??
?????@PostMapping("/add")
?????public?void?add(@RequestBody?category)?{
????????categoryService.add(category);
?????}
}
這樣就能開啟異步功能了。
是不是很easy?
但有個(gè)壞消息是:用@Async注解開啟的異步功能,會(huì)調(diào)用AsyncExecutionAspectSupport類的doSubmit方法。
默認(rèn)情況會(huì)走else邏輯。
而else的邏輯最終會(huì)調(diào)用doExecute方法:
protected?void?doExecute(Runnable?task)?{
??Thread?thread?=?(this.threadFactory?!=?null???this.threadFactory.newThread(task)?:?createThread(task));
??thread.start();
}
我去,這不是每次都會(huì)創(chuàng)建一個(gè)新線程嗎?
沒(méi)錯(cuò),使用@Async注解開啟的異步功能,默認(rèn)情況下,每次都會(huì)創(chuàng)建一個(gè)新線程。
如果在高并發(fā)的場(chǎng)景下,可能會(huì)產(chǎn)生大量的線程,從而導(dǎo)致OOM問(wèn)題。
建議大家在@Async注解開啟的異步功能時(shí),請(qǐng)別忘了定義一個(gè)
線程池。
9. 自旋鎖浪費(fèi)cpu資源
在并發(fā)編程中,自旋鎖想必大家都已經(jīng)耳熟能詳了。
自旋鎖有個(gè)非常經(jīng)典的使用場(chǎng)景就是:CAS(即比較和交換),它是一種無(wú)鎖化思想(說(shuō)白了用了一個(gè)死循環(huán)),用來(lái)解決高并發(fā)場(chǎng)景下,更新數(shù)據(jù)的問(wèn)題。
而atomic包下的很多類,比如:AtomicInteger、AtomicLong、AtomicBoolean等,都是用CAS實(shí)現(xiàn)的。
我們以AtomicInteger類為例,它的incrementAndGet沒(méi)有每次都給變量加1。
public?final?int?incrementAndGet()?{
????return?unsafe.getAndAddInt(this,?valueOffset,?1)?+?1;
}
它的底層就是用的自旋鎖實(shí)現(xiàn)的:
public?final?int?getAndAddInt(Object?var1,?long?var2,?int?var4)?{
??int?var5;
??do?{
??????var5?=?this.getIntVolatile(var1,?var2);
??}?while(!this.compareAndSwapInt(var1,?var2,?var5,?var5?+?var4));
????return?var5;
}
在do...while死循環(huán)中,不停進(jìn)行數(shù)據(jù)的比較和交換,如果一直失敗,則一直循環(huán)重試。
如果在高并發(fā)的情況下,compareAndSwapInt會(huì)很大概率失敗,因此導(dǎo)致了此處cpu不斷的自旋,這樣會(huì)嚴(yán)重浪費(fèi)cpu資源。
那么,如果解決這個(gè)問(wèn)題呢?
答:使用LockSupport類的parkNanos方法。
具體代碼如下:
private?boolean?compareAndSwapInt2(Object?var1,?long?var2,?int?var4,?int?var5)?{
?????if(this.compareAndSwapInt(var1,var2,var4,?var5))?{
??????????return?true;
??????}?else?{
??????????LockSupport.parkNanos(10);
??????????return?false;
??????}
?}
當(dāng)cas失敗之后,調(diào)用LockSupport類的parkNanos方法休眠一下,相當(dāng)于調(diào)用了Thread.Sleep方法。這樣能夠有效的減少頻繁自旋導(dǎo)致cpu資源過(guò)度浪費(fèi)的問(wèn)題。
10. ThreadLocal用完沒(méi)清空
在java中保證線程安全的技術(shù)有很多,可以使用synchroized、Lock等關(guān)鍵字給代碼塊加鎖。
但是它們有個(gè)共同的特點(diǎn),就是加鎖會(huì)對(duì)代碼的性能有一定的損耗。
其實(shí),在jdk中還提供了另外一種思想即:用空間換時(shí)間。
沒(méi)錯(cuò),使用ThreadLocal類就是對(duì)這種思想的一種具體體現(xiàn)。
ThreadLocal為每個(gè)使用變量的線程提供了一個(gè)獨(dú)立的變量副本,這樣每一個(gè)線程都能獨(dú)立地改變自己的副本,而不會(huì)影響其它線程所對(duì)應(yīng)的副本。
ThreadLocal的用法大致是這樣的:
先創(chuàng)建一個(gè)CurrentUser類,其中包含了ThreadLocal的邏輯。
public?class?CurrentUser?{
????private?static?final?ThreadLocal?THREA_LOCAL?=?new?ThreadLocal();
????
????public?static?void?set(UserInfo?userInfo)?{
????????THREA_LOCAL.set(userInfo);
????}
????
????public?static?UserInfo?get()?{
???????THREA_LOCAL.get();
????}
????
????public?static?void?remove()?{
???????THREA_LOCAL.remove();
????}
}
在業(yè)務(wù)代碼中調(diào)用CurrentUser類。
public?void?doSamething(UserDto?userDto)?{
???UserInfo?userInfo?=?convert(userDto);
???CurrentUser.set(userInfo);
???...
???//業(yè)務(wù)代碼
???UserInfo?userInfo?=?CurrentUser.get();
???...
}
在業(yè)務(wù)代碼的第一行,將userInfo對(duì)象設(shè)置到CurrentUser,這樣在業(yè)務(wù)代碼中,就能通過(guò)CurrentUser.get()獲取到剛剛設(shè)置的userInfo對(duì)象。特別是對(duì)業(yè)務(wù)代碼調(diào)用層級(jí)比較深的情況,這種用法非常有用,可以減少很多不必要傳參。
但在高并發(fā)的場(chǎng)景下,這段代碼有問(wèn)題,只往ThreadLocal存數(shù)據(jù),數(shù)據(jù)用完之后并沒(méi)有及時(shí)清理。
ThreadLocal即使使用了WeakReference(弱引用)也可能會(huì)存在內(nèi)存泄露問(wèn)題,因?yàn)?entry對(duì)象中只把key(即threadLocal對(duì)象)設(shè)置成了弱引用,但是value值沒(méi)有。
那么,如何解決這個(gè)問(wèn)題呢?
public?void?doSamething(UserDto?userDto)?{
???UserInfo?userInfo?=?convert(userDto);
???
???try{
?????CurrentUser.set(userInfo);
?????...
?????
?????//業(yè)務(wù)代碼
?????UserInfo?userInfo?=?CurrentUser.get();
?????...
???}?finally?{
??????CurrentUser.remove();
???}
}
需要在finally代碼塊中,調(diào)用remove方法清理沒(méi)用的數(shù)據(jù)。
