1. 【強(qiáng)制】獲取單例對(duì)象需要保證線程安全或南,其中的方法也要保證線程安全。
說(shuō)明:資源驅(qū)動(dòng)類寥闪、工具類敬飒、單例工廠類都需要注意。
2. 【強(qiáng)制】創(chuàng)建線程或線程池時(shí)請(qǐng)指定有意義的線程名稱硫豆,方便出錯(cuò)時(shí)回溯龙巨。
正例:自定義線程工廠,并且根據(jù)外部特征進(jìn)行分組熊响,比如旨别,來(lái)自同一機(jī)房的調(diào)用,把機(jī)房編號(hào)賦值給
whatFeatureOfGroup:
public class UserThreadFactory implements ThreadFactory {
private final String namePrefix;
private final AtomicInteger nextId = new AtomicInteger(1);
// 定義線程組名稱汗茄,在利用 jstack 來(lái)排查問(wèn)題時(shí)秸弛,非常有幫助
UserThreadFactory(String whatFeatureOfGroup) {
namePrefix = "FromUserThreadFactory's" + whatFeatureOfGroup + "-Worker-";
}
@Override
public Thread newThread(Runnable task) {
String name = namePrefix + nextId.getAndIncrement();
Thread thread = new Thread(null, task, name, 0, false);
System.out.println(thread.getName());
return thread;
}
}
3. 【強(qiáng)制】線程資源必須通過(guò)線程池提供,不允許在應(yīng)用中自行顯式創(chuàng)建線程洪碳。
說(shuō)明:線程池的好處是減少在創(chuàng)建和銷毀線程上所消耗的時(shí)間以及系統(tǒng)資源的開(kāi)銷递览,解決資源不足的問(wèn)題。如果不使用
線程池瞳腌,有可能造成系統(tǒng)創(chuàng)建大量同類線程而導(dǎo)致消耗完內(nèi)存或者“過(guò)度切換”的問(wèn)題绞铃。
我的筆記:使用線程池緩存線程可以提高效率,另外線程池幫我們做了管理線程的事情嫂侍,提供了優(yōu)雅關(guān)機(jī)儿捧、interrupt 等待 IO 的線程冷离,飽和策略等功能。
4. 【強(qiáng)制】線程池不允許使用 Executors 去創(chuàng)建纯命,而是通過(guò) ThreadPoolExecutor 的方式,這樣的處理方
式讓寫的同學(xué)更加明確線程池的運(yùn)行規(guī)則痹栖,規(guī)避資源耗盡的風(fēng)險(xiǎn)亿汞。
說(shuō)明:Executors 返回的線程池對(duì)象的弊端如下:
1)FixedThreadPool 和 SingleThreadPool:
允許的請(qǐng)求隊(duì)列長(zhǎng)度為 Integer.MAX_VALUE
,可能會(huì)堆積大量的請(qǐng)求揪阿,從而導(dǎo)致 OOM疗我。
2)CachedThreadPool:
允許的創(chuàng)建線程數(shù)量為 Integer.MAX_VALUE
,可能會(huì)創(chuàng)建大量的線程南捂,從而導(dǎo)致 OOM吴裤。
3)ScheduledThreadPool:
允許的請(qǐng)求隊(duì)列長(zhǎng)度為 Integer.MAX_VALUE
,可能會(huì)堆積大量的請(qǐng)求溺健,從而導(dǎo)致 OOM麦牺。
5. 【強(qiáng)制】SimpleDateFormat 是線程不安全的類,一般不要定義為 static 變量鞭缭,如果定義為 static剖膳,必須
加鎖,或者使用 DateUtils 工具類岭辣。
正例:注意線程安全吱晒,使用 DateUtils。亦推薦如下處理:
private static final ThreadLocal<DateFormat> dateStyle = new ThreadLocal<DateFormat>() {
@Override
protected DateFormat initialValue() {
return new SimpleDateFormat("yyyy-MM-dd");
}
};
說(shuō)明:如果是 JDK8 的應(yīng)用沦童,可以使用 Instant 代替 Date仑濒,LocalDateTime 代替 Calendar,DateTimeFormatter 代替
SimpleDateFormat偷遗,官方給出的解釋:simple beautiful strong immutable thread-safe墩瞳。
6. 【強(qiáng)制】必須回收自定義的 ThreadLocal 變量記錄的當(dāng)前線程的值,尤其在線程池場(chǎng)景下鹦肿,線程經(jīng)常會(huì)
被復(fù)用矗烛,如果不清理自定義的 ThreadLocal 變量,可能會(huì)影響后續(xù)業(yè)務(wù)邏輯和造成內(nèi)存泄露等問(wèn)題箩溃。
盡量在代碼中使用 try-finally 塊進(jìn)行回收瞭吃。
正例:
objectThreadLocal.set(userInfo);
try {
// ...
} finally {
objectThreadLocal.remove();
}
7. 【強(qiáng)制】高并發(fā)時(shí),同步調(diào)用應(yīng)該去考量鎖的性能損耗涣旨。能用無(wú)鎖數(shù)據(jù)結(jié)構(gòu)歪架,就不要用鎖;能鎖區(qū)塊霹陡,就
不要鎖整個(gè)方法體和蚪;能用對(duì)象鎖止状,就不要用類鎖。
說(shuō)明:盡可能使加鎖的代碼塊工作量盡可能的小攒霹,避免在鎖代碼塊中調(diào)用 RPC 方法怯疤。
筆記:優(yōu)先無(wú)鎖,不用鎖能解決的一定不要用鎖催束,即使用鎖也要控制粒度集峦,越細(xì)越好。
8. 【強(qiáng)制】對(duì)多個(gè)資源抠刺、數(shù)據(jù)庫(kù)表塔淤、對(duì)象同時(shí)加鎖時(shí),需要保持一致的加鎖順序速妖,否則可能會(huì)造成死鎖高蜂。
說(shuō)明:線程一需要對(duì)表 A、B罕容、C 依次全部加鎖后才可以進(jìn)行更新操作备恤,那么線程二的加鎖順序也必須是 A、B锦秒、C烘跺,否則可
能出現(xiàn)死鎖。
筆記:解決死鎖的方法:按順序鎖資源脂崔、超時(shí)滤淳、優(yōu)先級(jí)、死鎖檢測(cè)等砌左〔备溃可參考哲學(xué)家進(jìn)餐問(wèn)題學(xué)習(xí)更深入的并發(fā)機(jī)制。
9. 【強(qiáng)制】在使用阻塞等待獲取鎖的方式中汇歹,必須在 try 代碼塊之外屁擅,并且在加鎖方法與 try 代碼塊之間沒(méi)
有任何可能拋出異常的方法調(diào)用,避免加鎖成功后产弹,在 finally 中無(wú)法解鎖派歌。
說(shuō)明一:在 lock 方法與 try 代碼塊之間的方法調(diào)用拋出異常,無(wú)法解鎖痰哨,造成其它線程無(wú)法成功獲取鎖胶果。
說(shuō)明二:如果 lock 方法在 try 代碼塊之內(nèi),可能由于其它方法拋出異常斤斧,導(dǎo)致在 finally 代碼塊中早抠,unlock 對(duì)未加鎖的對(duì)
象解鎖,它會(huì)調(diào)用 AQS 的 tryRelease 方法(取決于具體實(shí)現(xiàn)類)撬讽,拋出 IllegalMonitorStateException 異常蕊连。
說(shuō)明三:在 Lock 對(duì)象的 lock 方法實(shí)現(xiàn)中可能拋出 unchecked 異常悬垃,產(chǎn)生的后果與說(shuō)明二相同。
正例:
Lock lock = new XxxLock();
// ...
lock.lock();
try {
doSomething();
doOthers();
} finally {
lock.unlock();
}
反例:
Lock lock = new XxxLock();
// ...
try {
// 如果此處拋出異常甘苍,則直接執(zhí)行 finally 代碼塊
doSomething();
// 無(wú)論加鎖是否成功尝蠕,finally 代碼塊都會(huì)執(zhí)行
lock.lock();
doOthers();
} finally {
lock.unlock();
}
10\. 【強(qiáng)制】在使用嘗試機(jī)制來(lái)獲取鎖的方式中,進(jìn)入業(yè)務(wù)代碼塊之前载庭,必須先判斷當(dāng)前線程是否持有鎖趟佃。
鎖的釋放規(guī)則與鎖的阻塞等待方式相同。
說(shuō)明:Lock 對(duì)象的 unlock 方法在執(zhí)行時(shí)昧捷,它會(huì)調(diào)用 AQS 的 tryRelease 方法(取決于具體實(shí)現(xiàn)類),如果當(dāng)前線程不
持有鎖罐寨,則拋出 IllegalMonitorStateException 異常靡挥。
正例:
```java
Lock lock = new XxxLock();
// ...
boolean isLocked = lock.tryLock();
if (isLocked) {
try {
doSomething();
doOthers();
} finally {
lock.unlock();
}
}
11. 【強(qiáng)制】并發(fā)修改同一記錄時(shí),避免更新丟失鸯绿,需要加鎖跋破。要么在應(yīng)用層加鎖,要么在緩存加鎖瓶蝴,要么
在數(shù)據(jù)庫(kù)層使用樂(lè)觀鎖毒返,使用 version 作為更新依據(jù)。
說(shuō)明:如果每次訪問(wèn)沖突概率小于 20%舷手,推薦使用樂(lè)觀鎖拧簸,否則使用悲觀鎖。樂(lè)觀鎖的重試次數(shù)不得小于 3 次男窟。
12. 【強(qiáng)制】多線程并行處理定時(shí)任務(wù)時(shí)盆赤,Timer 運(yùn)行多個(gè) TimeTask 時(shí),只要其中之一沒(méi)有捕獲拋出的異
常歉眷,其它任務(wù)便會(huì)自動(dòng)終止運(yùn)行牺六,使用 ScheduledExecutorService 則沒(méi)有這個(gè)問(wèn)題。
13.【推薦】資金相關(guān)的金融敏感信息汗捡,使用悲觀鎖策略淑际。
說(shuō)明:樂(lè)觀鎖在獲得鎖的同時(shí)已經(jīng)完成了更新操作,校驗(yàn)邏輯容易出現(xiàn)漏洞扇住,另外春缕,樂(lè)觀鎖對(duì)沖突的解決策略有較復(fù)雜
的要求,處理不當(dāng)容易造成系統(tǒng)壓力或數(shù)據(jù)異常艘蹋,所以資金相關(guān)的金融敏感信息不建議使用樂(lè)觀鎖更新淡溯。
正例:悲觀鎖遵循一鎖二判三更新四釋放的原則。
14.【推薦】使用 CountDownLatch 進(jìn)行異步轉(zhuǎn)同步操作簿训,每個(gè)線程退出前必須調(diào)用 countDown 方法咱娶,線
程執(zhí)行代碼注意 catch 異常米间,確保 countDown 方法被執(zhí)行到,避免主線程無(wú)法執(zhí)行至 await 方法膘侮,
直到超時(shí)才返回結(jié)果屈糊。
說(shuō)明:注意,子線程拋出異常堆棧琼了,不能在主線程 try-catch 到逻锐。
筆記:CountDownLatch 存在于 java.util.concurrent 包下。這個(gè)類能夠使一個(gè)線程等待其他線程完成各自的工作后再執(zhí)行雕薪。請(qǐng)?jiān)?try...finally 語(yǔ)句里執(zhí)行 countDown 方法昧诱,與關(guān)閉資源類似。
15.【推薦】避免 Random 實(shí)例被多線程使用所袁,雖然共享該實(shí)例是線程安全的盏档,但會(huì)因競(jìng)爭(zhēng)同一 seed 導(dǎo)致
的性能下降。
說(shuō)明:Random 實(shí)例包括 java.util.Random 的實(shí)例或者 Math.random() 的方式燥爷。
正例:在 JDK7 之后蜈亩,可以直接使用 API ThreadLocalRandom,而在 JDK7 之前前翎,需要編碼保證每個(gè)線程持有一個(gè)
單獨(dú)的 Random 實(shí)例稚配。
16.【推薦】通過(guò)雙重檢查鎖(double-checked locking),實(shí)現(xiàn)延遲初始化需要將目標(biāo)屬性聲明為
volatile 型港华,(比如修改 helper 的屬性聲明為 private volatile Helper helper = null;)道川。
正例:
public class LazyInitDemo {
private volatile Helper helper = null;
public Helper getHelper() {
if (helper == null) {
synchronized(this) {
if (helper == null) {
helper = new Helper();
}
}
}
return helper;
}
// other methods and fields...
}
筆記:請(qǐng)參考參考The "Double-Checked Locking is Broken" Declaration
17.【參考】volatile 解決多線程內(nèi)存不可見(jiàn)問(wèn)題對(duì)于一寫多讀,是可以解決變量同步問(wèn)題立宜,但是如果多
寫愤惰,同樣無(wú)法解決線程安全問(wèn)題。
說(shuō)明:如果是 count++ 操作赘理,使用如下類實(shí)現(xiàn):
AtomicInteger count = new AtomicInteger();
count.addAndGet(1);
如果是 JDK8宦言,推薦使用 LongAdder 對(duì)象,比 AtomicLong 性能更好(減少樂(lè)觀鎖的重試次數(shù))商模。
筆記:volatile只有內(nèi)存可見(jiàn)性語(yǔ)義奠旺,synchronized有互斥語(yǔ)義,一寫多讀使用volatile就可以施流,多寫就必須使用synchronized响疚,fetch-mod-get也必須使用synchronized。
18. 【參考】HashMap 在容量不夠進(jìn)行 resize 時(shí)由于高并發(fā)可能出現(xiàn)死鏈瞪醋,導(dǎo)致 CPU 飆升忿晕,在開(kāi)發(fā)過(guò)程
中注意規(guī)避此風(fēng)險(xiǎn)。
19. 【參考】ThreadLocal 對(duì)象使用 static 修飾银受,ThreadLocal 無(wú)法解決共享對(duì)象的更新問(wèn)題践盼。
說(shuō)明:這個(gè)變量是針對(duì)一個(gè)線程內(nèi)所有操作共享的鸦采,所以設(shè)置為靜態(tài)變量,所有此類實(shí)例共享此靜態(tài)變量咕幻,也就是說(shuō)在
類第一次被使用時(shí)裝載渔伯,只分配一塊存儲(chǔ)空間,所有此類的對(duì)象(只要是這個(gè)線程內(nèi)定義的)都可以操控這個(gè)變量肄程。
筆記:ThreadLocal 為解決多線程程序的并發(fā)問(wèn)題提供了一種新思路锣吼。當(dāng)使用ThreadLocal維護(hù)變量時(shí),ThreadLocal為每個(gè)使用該變量的線程提供獨(dú)立的變量副本蓝厌,所以每一個(gè)線程都可以獨(dú)立地改變自己的副本玄叠,而不會(huì)影響其它線程所對(duì)應(yīng)的副本。ThreadLocal實(shí)際上是一個(gè)從線程ID到變量的Map拓提,每次取得ThreadLocal變量读恃,實(shí)際上是先取得當(dāng)前線程ID,再用當(dāng)前線程ID取得關(guān)聯(lián)的變量崎苗。ThreadLocal 使用了 WeakHashMap,在 key 被回收的時(shí)候舀寓,value 也被回收了胆数,不用擔(dān)心內(nèi)存泄露。
參考
- 2022 Java開(kāi)發(fā)手冊(cè)(黃山版).pdf
- 《編寫高質(zhì)量代碼:改善Java程序的151個(gè)建議》
- 白話阿里巴巴Java開(kāi)發(fā)手冊(cè)(安全規(guī)約) - 李艷鵬 - 簡(jiǎn)書(shū)(http://www.reibang.com/p/9528c4ea1504)
- Java并發(fā)編程的藝術(shù)