阿里開發手冊泰山版學習筆記七、編程規約-併發處理

  1. 【強制】獲取單例對象需要保證線程安全,其中的方法也要保證線程安全。
    說明:資源驅動類、工具類、單例工廠類都需要注意。

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


whatFeaturOfGroup
public class UserThreadFactory implements ThreadFactory {
 private final String namePrefix;
 private final AtomicInteger nextId = new AtomicInteger(1);
 // 定義線程組名稱,在 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;
 }
}

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

  2. 【強制】線程池不允許使用 Executors 去創建,而是通過 ThreadPoolExecutor 的方式,這
    樣的處理方式讓寫的同學更加明確線程池的運行規則,規避資源耗盡的風險。
    說明:Executors 返回的線程池對象的弊端如下:
    1) FixedThreadPool 和 SingleThreadPool:
    允許的請求隊列長度爲 Integer.MAX_VALUE,可能會堆積大量的請求,從而導致 OOM。
    2) CachedThreadPool:
    允許的創建線程數量爲 Integer.MAX_VALUE,可能會創建大量的線程,從而導致 OOM。

  3. 【強制】SimpleDateFormat 是線程不安全的類,一般不要定義爲 static 變量,如果定義爲 static,必須加鎖,或者使用 DateUtils 工具類。
    正例:注意線程安全,使用 DateUtils。亦推薦如下處理:


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 immutablethread-safe。

  1. 【強制】必須回收自定義的 ThreadLocal 變量,尤其在線程池場景下,線程經常會被複用,如果不清理自定義的 ThreadLocal 變量,可能會影響後續業務邏輯和造成內存泄露等問題。儘量在代理中使用 try-finally 塊進行回收。
    正例:

objectThreadLocal.set(userInfo);
try {
 // ...
} finally {
 objectThreadLocal.remove();
}

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

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

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


Lock lock = new XxxLock();
// ...
lock.lock();
try {
 doSomething();
 doOthers();
} finally {
 lock.unlock();
}

反例:


Lock lock = new XxxLock();
// ...
try {
 // 如果此處拋出異常,則直接執行 finally 代碼塊
 doSomething();
 // 無論加鎖是否成功,finally 代碼塊都會執行
 lock.lock();
 doOthers();
} finally {
 lock.unlock();
}

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

Lock lock = new XxxLock();
// ...
boolean isLocked = lock.tryLock();
if (isLocked) {
 try {
 doSomething();
 doOthers();
 } finally {
 lock.unlock();
 }
}

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

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

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

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

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

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

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...
}

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

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

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章