1. 【強(qiáng)制】獲取單例對(duì)象需要保證線程安全,其中的方法也要保證線程安全。
說明:資源驅(qū)動(dòng)類、工具類、單例工廠類都需要注意。
2. 【強(qiáng)制】創(chuàng)建線程或線程池時(shí)請(qǐng)指定有意義的線程名稱,方便出錯(cuò)時(shí)回溯。
正例:自定義線程工廠,并且根據(jù)外部特征進(jìn)行分組,比如,來自同一機(jī)房的調(diào)用,把機(jī)房編號(hào)賦值給
whatFeatureOfGroup:
public class UserThreadFactory implements ThreadFactory {
private final String namePrefix;
private final AtomicInteger nextId = new AtomicInteger(1);
// 定義線程組名稱,在利用 jstack 來排查問題時(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)制】線程資源必須通過線程池提供,不允許在應(yīng)用中自行顯式創(chuàng)建線程。
說明:線程池的好處是減少在創(chuàng)建和銷毀線程上所消耗的時(shí)間以及系統(tǒng)資源的開銷,解決資源不足的問題。如果不使用
線程池,有可能造成系統(tǒng)創(chuàng)建大量同類線程而導(dǎo)致消耗完內(nèi)存或者“過度切換”的問題。
我的筆記:使用線程池緩存線程可以提高效率,另外線程池幫我們做了管理線程的事情,提供了優(yōu)雅關(guān)機(jī)、interrupt 等待 IO 的線程,飽和策略等功能。
4. 【強(qiáng)制】線程池不允許使用 Executors 去創(chuàng)建,而是通過 ThreadPoolExecutor 的方式,這樣的處理方
式讓寫的同學(xué)更加明確線程池的運(yùn)行規(guī)則,規(guī)避資源耗盡的風(fēng)險(xiǎn)。
說明: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");
}
};
說明:如果是 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)存泄露等問題。
盡量在代碼中使用 try-finally 塊進(jìn)行回收。
正例:
objectThreadLocal.set(userInfo);
try {
// ...
} finally {
objectThreadLocal.remove();
}
7. 【強(qiáng)制】高并發(fā)時(shí),同步調(diào)用應(yīng)該去考量鎖的性能損耗。能用無鎖數(shù)據(jù)結(jié)構(gòu),就不要用鎖;能鎖區(qū)塊,就
不要鎖整個(gè)方法體;能用對(duì)象鎖,就不要用類鎖。
說明:盡可能使加鎖的代碼塊工作量盡可能的小,避免在鎖代碼塊中調(diào)用 RPC 方法。
筆記:優(yōu)先無鎖,不用鎖能解決的一定不要用鎖,即使用鎖也要控制粒度,越細(xì)越好。
8. 【強(qiáng)制】對(duì)多個(gè)資源、數(shù)據(jù)庫表、對(duì)象同時(shí)加鎖時(shí),需要保持一致的加鎖順序,否則可能會(huì)造成死鎖。
說明:線程一需要對(duì)表 A、B、C 依次全部加鎖后才可以進(jìn)行更新操作,那么線程二的加鎖順序也必須是 A、B、C,否則可
能出現(xiàn)死鎖。
筆記:解決死鎖的方法:按順序鎖資源、超時(shí)、優(yōu)先級(jí)、死鎖檢測(cè)等。可參考哲學(xué)家進(jìn)餐問題學(xué)習(xí)更深入的并發(fā)機(jī)制。
9. 【強(qiáng)制】在使用阻塞等待獲取鎖的方式中,必須在 try 代碼塊之外,并且在加鎖方法與 try 代碼塊之間沒
有任何可能拋出異常的方法調(diào)用,避免加鎖成功后,在 finally 中無法解鎖。
說明一:在 lock 方法與 try 代碼塊之間的方法調(diào)用拋出異常,無法解鎖,造成其它線程無法成功獲取鎖。
說明二:如果 lock 方法在 try 代碼塊之內(nèi),可能由于其它方法拋出異常,導(dǎo)致在 finally 代碼塊中,unlock 對(duì)未加鎖的對(duì)
象解鎖,它會(huì)調(diào)用 AQS 的 tryRelease 方法(取決于具體實(shí)現(xiàn)類),拋出 IllegalMonitorStateException 異常。
說明三:在 Lock 對(duì)象的 lock 方法實(shí)現(xiàn)中可能拋出 unchecked 異常,產(chǎn)生的后果與說明二相同。
正例:
Lock lock = new XxxLock();
// ...
lock.lock();
try {
doSomething();
doOthers();
} finally {
lock.unlock();
}
反例:
Lock lock = new XxxLock();
// ...
try {
// 如果此處拋出異常,則直接執(zhí)行 finally 代碼塊
doSomething();
// 無論加鎖是否成功,finally 代碼塊都會(huì)執(zhí)行
lock.lock();
doOthers();
} finally {
lock.unlock();
}
10\. 【強(qiáng)制】在使用嘗試機(jī)制來獲取鎖的方式中,進(jìn)入業(yè)務(wù)代碼塊之前,必須先判斷當(dāng)前線程是否持有鎖。
鎖的釋放規(guī)則與鎖的阻塞等待方式相同。
說明: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ù)庫層使用樂觀鎖,使用 version 作為更新依據(jù)。
說明:如果每次訪問沖突概率小于 20%,推薦使用樂觀鎖,否則使用悲觀鎖。樂觀鎖的重試次數(shù)不得小于 3 次。
12. 【強(qiáng)制】多線程并行處理定時(shí)任務(wù)時(shí),Timer 運(yùn)行多個(gè) TimeTask 時(shí),只要其中之一沒有捕獲拋出的異
常,其它任務(wù)便會(huì)自動(dòng)終止運(yùn)行,使用 ScheduledExecutorService 則沒有這個(gè)問題。
13.【推薦】資金相關(guān)的金融敏感信息,使用悲觀鎖策略。
說明:樂觀鎖在獲得鎖的同時(shí)已經(jīng)完成了更新操作,校驗(yàn)邏輯容易出現(xiàn)漏洞,另外,樂觀鎖對(duì)沖突的解決策略有較復(fù)雜
的要求,處理不當(dāng)容易造成系統(tǒng)壓力或數(shù)據(jù)異常,所以資金相關(guān)的金融敏感信息不建議使用樂觀鎖更新。
正例:悲觀鎖遵循一鎖二判三更新四釋放的原則。
14.【推薦】使用 CountDownLatch 進(jìn)行異步轉(zhuǎn)同步操作,每個(gè)線程退出前必須調(diào)用 countDown 方法,線
程執(zhí)行代碼注意 catch 異常,確保 countDown 方法被執(zhí)行到,避免主線程無法執(zhí)行至 await 方法,
直到超時(shí)才返回結(jié)果。
說明:注意,子線程拋出異常堆棧,不能在主線程 try-catch 到。
筆記:CountDownLatch 存在于 java.util.concurrent 包下。這個(gè)類能夠使一個(gè)線程等待其他線程完成各自的工作后再執(zhí)行。請(qǐng)?jiān)?try...finally 語句里執(zhí)行 countDown 方法,與關(guān)閉資源類似。
15.【推薦】避免 Random 實(shí)例被多線程使用,雖然共享該實(shí)例是線程安全的,但會(huì)因競(jìng)爭(zhēng)同一 seed 導(dǎo)致
的性能下降。
說明:Random 實(shí)例包括 java.util.Random 的實(shí)例或者 Math.random() 的方式。
正例:在 JDK7 之后,可以直接使用 API ThreadLocalRandom,而在 JDK7 之前,需要編碼保證每個(gè)線程持有一個(gè)
單獨(dú)的 Random 實(shí)例。
16.【推薦】通過雙重檢查鎖(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)存不可見問題對(duì)于一寫多讀,是可以解決變量同步問題,但是如果多
寫,同樣無法解決線程安全問題。
說明:如果是 count++ 操作,使用如下類實(shí)現(xiàn):
AtomicInteger count = new AtomicInteger();
count.addAndGet(1);
如果是 JDK8,推薦使用 LongAdder 對(duì)象,比 AtomicLong 性能更好(減少樂觀鎖的重試次數(shù))。
筆記:volatile只有內(nèi)存可見性語義,synchronized有互斥語義,一寫多讀使用volatile就可以,多寫就必須使用synchronized,fetch-mod-get也必須使用synchronized。
18. 【參考】HashMap 在容量不夠進(jìn)行 resize 時(shí)由于高并發(fā)可能出現(xiàn)死鏈,導(dǎo)致 CPU 飆升,在開發(fā)過程
中注意規(guī)避此風(fēng)險(xiǎn)。
19. 【參考】ThreadLocal 對(duì)象使用 static 修飾,ThreadLocal 無法解決共享對(duì)象的更新問題。
說明:這個(gè)變量是針對(duì)一個(gè)線程內(nèi)所有操作共享的,所以設(shè)置為靜態(tài)變量,所有此類實(shí)例共享此靜態(tài)變量,也就是說在
類第一次被使用時(shí)裝載,只分配一塊存儲(chǔ)空間,所有此類的對(duì)象(只要是這個(gè)線程內(nèi)定義的)都可以操控這個(gè)變量。
筆記:ThreadLocal 為解決多線程程序的并發(fā)問題提供了一種新思路。當(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開發(fā)手冊(cè)(黃山版).pdf
- 《編寫高質(zhì)量代碼:改善Java程序的151個(gè)建議》
- 白話阿里巴巴Java開發(fā)手冊(cè)(安全規(guī)約) - 李艷鵬 - 簡(jiǎn)書(http://www.lxweimin.com/p/9528c4ea1504)
- Java并發(fā)編程的藝術(shù)