Like Share Discussion Bookmark Smile

J.J. Huang   2020-05-04   Java   瀏覽次數:

《阿里Java開發手冊》 | 編程規約 - 並發處理

【強制】獲取單例對象需要保證執行緒(thread)安全,其中的方法也要保證執行緒(thread)安全。
說明:資源驅動類、工具類、單例工廠類都需要注意。


【強制】建立執行緒(thread)或執行緒池(thread pool)時請指定有意義的執行緒(thread)名稱,方便出錯時回溯。
正例:自定義執行緒(thread)工廠,並且根據外部特徵進行分組,比如,來自同一機房的調用,把機房編號賦值給whatFeaturOfGroup

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class UserThreadFactory implements ThreadFactory {
private final String namePrefix;
private final AtomicInteger nextId = new AtomicInteger(1);
// 定義執行緒(thread)組名稱,在 jstack 問題排查時,非常有幫助
UserThreadFactory(String whatFeaturOfGroup) {
namePrefix = "From UserThreadFactory's " + whatFeaturOfGroup + "-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;
}
}

【強制】執行緒(thread)資源必須通過執行緒池(thread pool)提供,不允許在應用中自行顯式建立執行緒(thread)。
說明:執行緒池(thread pool)的好處是減少在建立和銷毀執行緒(thread)上所消耗的時間以及系統資源的開銷,解決資源不足的問題。如果不使用執行緒池(thread pool),有可能造成系統建立大量同類執行緒(thread)而導致消耗完內存或者“過度切換”的問題。


【強制】執行緒池(thread pool)不允許使用 Executors 去建立,而是通過 ThreadPoolExecutor 的方式,這樣的處理方式讓寫的同學更加明確執行緒池(thread pool)的運行規則,規避資源耗盡的風險。
說明:Executors 返回的執行緒池(thread pool)對象的弊端如下:

  • 1.FixedThreadPool 和 SingleThreadPool:
    允許的請求隊列長度為 Integer.MAX_VALUE,可能會堆積大量的請求,從而導致 OOM。
  • 2.CachedThreadPool:
    允許的建立執行緒(thread)數量為 Integer.MAX_VALUE,可能會建立大量的執行緒(thread),從而導致 OOM。

【強制】SimpleDateFormat 是執行緒(thread)不安全的類,一般不要定義為 static 變數,如果定義為 static,必須加鎖,或者使用 DateUtils 工具類。
正例:注意執行緒(thread)安全,使用 DateUtils。亦推薦如下處理:

1
2
3
4
5
6
private static final ThreadLocal < DateFormat > df = new ThreadLocal < DateFormat > () {
@Override
protected DateFormat initialValue() {
return new SimpleDateFormat("yyyy-MM-dd");
}
};

說明:如果是 JDK8 的應用,可以使用 Instant 代替 Date,LocalDateTime 代替 Calendar,DateTimeFormatter 代替 SimpleDateFormat,官方給出的解釋:simple beautiful strong immutable thread-safe。


【強制】必須回收自定義的 ThreadLocal 變數,尤其在執行緒池(thread pool)場景下,執行緒(thread)經常會被復用,如果不清理自定義的 ThreadLocal 變數,可能會影響後續業務邏輯和造成內存洩露等問題。盡量在代理中使用 try-finally 塊進行回收。
正例:

1
2
3
4
5
6
objectThreadLocal.set(userInfo);
try {
// ...
} finally {
objectThreadLocal.remove();
}

【強制】高並發時,同步調用應該去考量鎖的性能損耗。能用無鎖資料結構,就不要用鎖;能鎖區塊,就不要鎖整個方法體;能用對象鎖,就不要用類鎖。
說明:盡可能使加鎖的程式碼塊工作量盡可能的小,避免在鎖程式碼塊中調用 RPC 方法。


【強制】對多個資源、資料庫表、對象同時加鎖時,需要保持一致的加鎖順序,否則可能會造成死鎖。
說明:執行緒(thread)一需要對錶 A、B、C 依次全部加鎖後才可以進行更新操作,那麼執行緒(thread)二的加鎖順序也必須是 A、 B、 C,否則可能出現死鎖。


【強制】在使用阻塞等待獲取鎖的方式中,必須在 try 程式碼塊之外,並且在加鎖方法與 try 程式碼塊之間沒有任何可能拋出異常的方法調用,避免加鎖成功後,在 finally 中無法解鎖。
說明一:如果在 lock 方法與 try 程式碼塊之間的方法調用拋出異常,那麼無法解鎖,造成其它執行緒(thread)無法成功獲取鎖。
說明二:如果 lock 方法在 try 程式碼塊之內,可能由於其它方法拋出異常,導致在 finally 程式碼塊中,unlock 對未加鎖的對象解鎖,它會調用 AQS 的 tryRelease 方法(取決於具體實作類),拋出 IllegalMonitorStateException 異常。
說明三:在 Lock 對象的 lock 方法實作中可能拋出 unchecked 異常,產生的後果與說明二相同。
正例:

1
2
3
4
5
6
7
8
9
Lock lock = new XxxLock();
// ...
lock.lock();
try {
doSomething();
doOthers();
} finally {
lock.unlock();
}

反例:

1
2
3
4
5
6
7
8
9
10
11
Lock lock = new XxxLock();
// ...
try {
// 如果此處拋出異常,則直接執行 finally 程式碼塊
doSomething();
// 無論加鎖是否成功,finally 程式碼塊都會執行
lock.lock();
doOthers();
} finally {
lock.unlock();
}

【強制】在使用嘗試機制來獲取鎖的方式中,進入業務程式碼塊之前,必須先判斷當前執行緒(thread)是否持有鎖。鎖的釋放規則與鎖的阻塞等待方式相同。
說明:Lock 對象的 unlock 方法在執行時,它會調用 AQS 的 tryRelease 方法(取決於具體實作類),如果當前執行緒(thread)不持有鎖,則拋出 IllegalMonitorStateException 異常。
正例:

1
2
3
4
5
6
7
8
9
10
11
Lock lock = new XxxLock();
// ...
boolean isLocked = lock.tryLock();
if (isLocked) {
try {
doSomething();
doOthers();
} finally {
lock.unlock();
}
}

【強制】並發修改同一記錄時,避免更新丟失,需要加鎖。要麼在應用層加鎖,要麼在緩存加鎖,要麼在資料庫層使用樂觀鎖,使用 version 作為更新依據。
說明:如果每次訪問衝突概率小於 20%,推薦使用樂觀鎖,否則使用悲觀鎖。樂觀鎖的重試次數不得小於 3 次。


【強制】多執行緒(thread)並行處理定時任務時,Timer 運行多個 TimeTask 時,只要其中之一沒有捕獲拋出的異常,其它任務便會自動終止運行,使用 ScheduledExecutorService 則沒有這個問題。


【推薦】資金相關的金融敏感訊息,使用悲觀鎖策略。
說明:樂觀鎖在獲得鎖的同時已經完成了更新操作,校驗邏輯容易出現漏洞,另外,樂觀鎖對沖突的解決策略有較複雜的要求,處理不當容易造成系統壓力或資料異常,所以資金相關的金融敏感訊息不建議使用樂觀鎖更新。
正例:悲觀鎖遵循一鎖二判三更新四釋放的原則


【推薦】使用 CountDownLatch 進行異步轉同步操作,每個執行緒(thread)退出前必須調用 countDown 方法,執行緒(thread)執行程式碼注意 catch 異常,確保 countDown 方法被執行到,避免主執行緒(thread)無法執行至 await 方法,直到超時才返回結果。
說明:注意,子執行緒(thread)拋出異常堆棧,不能在主執行緒(thread) try-catch 到。


【推薦】避免 Random 實例被多執行緒(thread)使用,雖然共享該實例是執行緒(thread)安全的,但會因競爭同一 seed 導致的性能下降。
說明:Random 實例包括 java.util.Random 的實例或者 Math.random()的方式。
正例:在 JDK7 之後,可以直接使用 API ThreadLocalRandom,而在 JDK7 之前,需要編碼保證每個線 程持有一個單獨的 Random 實例。


【推薦】通過雙重檢查鎖(double-checked locking)(在並發場景下)實作延遲初始化的優化問題隱患(可參考The “Double-Checked Locking is Broken” Declaration),推薦解決方案中較為簡單一種(適用於JDK5 及以上版本),將目標屬性聲明為 volatile 型(比如修改 helper 的屬性聲明為private volatile Helper helper = null;)。
反例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class LazyInitDemo {
private Helper helper = null;
public Helper getHelper() {
if (helper == null) {
synchronized(this) {
if (helper == null) {
helper = new Helper();
}
}
}
return helper;
}
// other methods and fields...
}

【參考】volatile 解決多執行緒(thread)內存不可見問題。對於一寫多讀,是可以解決變數同步問題,但 是如果多寫,同樣無法解決執行緒(thread)安全問題。
說明:如果是 count++ 操作,使用如下類實作:AtomicInteger count = new AtomicInteger(); count.addAndGet(1); 如果是 JDK8 ,推薦使用 LongAdder 對象,比 AtomicLong 性能更好(減少樂觀鎖的重試次數)。


【參考】HashMap 在容量不夠進行 resize 時由於高並發可能出現死鏈,導致 CPU 飆升,在 開發過程中註意規避此風險。


【參考】ThreadLocal 對象使用 static 修飾,ThreadLocal 無法解決共享對象的更新問題。
說明:這個變數是針對一個執行緒(thread)內所有操作共享的,所以設置為靜態變數,所有此類實例共享此靜態變數, 也就是說在類第一次被使用時裝載,只分配一塊存儲空間,所有此類的對象(只要是這個執行緒(thread)內定義的)都可以操控這個變數。


心得

看完這篇「並發處理」後,執行緒(thread)這塊,因為接觸使用的少,所以在實際應用上沒遇到什麼問題,因為開發的邏輯簡單不複雜;但沒想到有這麼多的小細節要注意,最多需要注意的稍微整理大致歸類為兩點「資源釋放」、「順序性(加解鎖)」。

結語

文章越看越多,技術越學越多,就會發現自己的不足;技術學到後面都會想要將基礎再重新在打得更加扎實。

以前在開發覺得理所當然的事情,例如:命名規則、命名規範,照著別人怎麼說就怎麼做的想法,並沒有好好去想為什麼要這樣設計和規範。
於是乎同事們推薦《阿里巴巴Java開發手冊》來做閱讀,書中提到種種規範《正確範例》、《錯誤範例》還有解釋定義說明;我相信在閱讀完這一系列後,一定會更加扎實且實在。

如對此書有興趣,建議去購買官方認證的書籍,給予官方支持。

註:如有侵權,通知即刪。


註:以上參考了
Alibaba-Java-Coding-Guidelines Github
Alibaba-Java-Coding-Guidelines English Version