Java開發(fā) 并發(fā)處理

2021-03-22 15:19 更新

\1. 【強制】獲取單例對象需要保證線程安全,其中的方法也要保證線程安全。

  • 說明:資源驅(qū)動類、工具類、單例工廠類都需要注意。

\2. 【強制】創(chuàng)建線程或線程池時請指定有意義的線程名稱,方便出錯時回溯。

  • 正例:自定義線程工廠,并且根據(jù)外部特征進行分組,比如,來自同一機房的調(diào)用,把機房編號賦值給 whatFeatureOfGroup

public class UserThreadFactory implements ThreadFactory {
    private final String namePrefix;
    private final AtomicInteger nextId = new AtomicInteger(1);
    // 定義線程組名稱,在利用 jstack 來排查問題時,非常有幫助
    UserThreadFactory(String whatFeatureOfGroup) {
        namePrefix = "From UserThreadFactory'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. 【強制】線程資源必須通過線程池提供,不允許在應用中自行顯式創(chuàng)建線程。

  • 說明:線程池的好處是減少在創(chuàng)建和銷毀線程上所消耗的時間以及系統(tǒng)資源的開銷,解決資源不足的問題。如果不使用線程池,有可能造成系統(tǒng)創(chuàng)建大量同類線程而導致消耗完內(nèi)存或者“過度切換”的問題。

\4. 【強制】線程池不允許使用 Executors 去創(chuàng)建,而是通過 ThreadPoolExecutor 的方式,這樣的處理方式讓寫的同學更加明確線程池的運行規(guī)則,規(guī)避資源耗盡的風險。

  • 說明:Executors 返回的線程池對象的弊端如下: 1) FixedThreadPool 和 SingleThreadPool: 允許的請求隊列長度為 Integer.MAX_VALUE,可能會堆積大量的請求,從而導致 OOM。 2) CachedThreadPool: 允許的創(chuàng)建線程數(shù)量為 Integer.MAX_VALUE,可能會創(chuàng)建大量的線程,從而導致 OOM。

\5. 【強制】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 immutable thread-safe。

\6. 【強制】必須回收自定義的 ThreadLocal 變量,尤其在線程池場景下,線程經(jīng)常會被復用,如果不清理自定義的 ThreadLocal 變量,可能會影響后續(xù)業(yè)務邏輯和造成內(nèi)存泄露等問題。盡量在代理中使用 try-finally 塊進行回收。

  • 正例:

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

\7. 【強制】高并發(fā)時,同步調(diào)用應該去考量鎖的性能損耗。能用無鎖數(shù)據(jù)結(jié)構(gòu),就不要用鎖;能鎖區(qū)塊,就不要鎖整個方法體;能用對象鎖,就不要用類鎖。

  • 說明:盡可能使加鎖的代碼塊工作量盡可能的小,避免在鎖代碼塊中調(diào)用 RPC 方法。

\8. 【強制】對多個資源、數(shù)據(jù)庫表、對象同時加鎖時,需要保持一致的加鎖順序,否則可能會造成死鎖。

  • 說明:線程一需要對表 A、B、C 依次全部加鎖后才可以進行更新操作,那么線程二的加鎖順序也必須是 A、 B、C,否則可能出現(xiàn)死鎖。

\9. 【強制】在使用阻塞等待獲取鎖的方式中,必須在 try 代碼塊之外,并且在加鎖方法與 try 代碼塊之間沒有任何可能拋出異常的方法調(diào)用,避免加鎖成功后,在 finally 中無法解鎖。

  • 說明一:如果在 lock 方法與 try 代碼塊之間的方法調(diào)用拋出異常,那么無法解鎖,造成其它線程無法成功獲取鎖。

  • 說明二:如果 lock 方法在 try 代碼塊之內(nèi),可能由于其它方法拋出異常,導致在 finally 代碼塊中,unlock 對未加鎖的對象解鎖,它會調(diào)用 AQS 的 tryRelease 方法(取決于具體實現(xiàn)類),拋出 IllegalMonitorStateException 異常。

  • 說明三:在 Lock 對象的 lock 方法實現(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 代碼塊都會執(zhí)行
    lock.lock();
    doOthers();
}
finally {
    lock.unlock();
}

\10. 【強制】在使用嘗試機制來獲取鎖的方式中,進入業(yè)務代碼塊之前,必須先判斷當前線程是否持有鎖。鎖的釋放規(guī)則與鎖的阻塞等待方式相同。

  • 說明:Lock 對象的 unlock 方法在執(zhí)行時,它會調(diào)用 AQS 的 tryRelease 方法(取決于具體實現(xiàn)類),如果當前線程不持有鎖,則拋出 IllegalMonitorStateException 異常。

  • 正例:

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

11.【強制】并發(fā)修改同一記錄時,避免更新丟失,需要加鎖。要么在應用層加鎖,要么在緩存加鎖,要么在數(shù)據(jù)庫層使用樂觀鎖,使用 version 作為更新依據(jù)。

  • 說明:如果每次訪問沖突概率小于 20%,推薦使用樂觀鎖,否則使用悲觀鎖。樂觀鎖的重試次數(shù)不得小于3 次。

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

13.【推薦】資金相關(guān)的金融敏感信息,使用悲觀鎖策略。

  • 說明:樂觀鎖在獲得鎖的同時已經(jīng)完成了更新操作,校驗邏輯容易出現(xiàn)漏洞,另外,樂觀鎖對沖突的解決策略有較復雜的要求,處理不當容易造成系統(tǒng)壓力或數(shù)據(jù)異常,所以資金相關(guān)的金融敏感信息不建議使用樂觀鎖更新。

  • 正例:悲觀鎖遵循一鎖、二判、三更新、四釋放的原則。

14.【推薦】使用 CountDownLatch 進行異步轉(zhuǎn)同步操作,每個線程退出前必須調(diào)用 countDown 方法,線程執(zhí)行代碼注意 catch 異常,確保 countDown 方法被執(zhí)行到,避免主線程無法執(zhí)行至await 方法,直到超時才返回結(jié)果。

  • 說明:注意,子線程拋出異常堆棧,不能在主線程 try-catch 到。

15.【推薦】避免 Random 實例被多線程使用,雖然共享該實例是線程安全的,但會因競爭同一 seed 導致的性能下降。

  • 說明:Random 實例包括 java.util.Random 的實例或者 Math.random()的方式。

  • 正例:在 JDK7 之后,可以直接使用 API ThreadLocalRandom,而在 JDK7 之前,需要編碼保證每個線程持有一個單獨的 Random 實例。

16.【推薦】通過雙重檢查鎖(double-checked locking)(在并發(fā)場景下)存在延遲初始化的優(yōu)化問題隱患(可參考 The "Double-Checked Locking is Broken" Declaration),推薦解決方案中較為簡單一種(適用于 JDK5 及以上版本),將目標屬性聲明為 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... 
}

17.【參考】volatile 解決多線程內(nèi)存不可見問題。對于一寫多讀,是可以解決變量同步問題,但是如果多寫,同樣無法解決線程安全問題。

  • 說明:如果是 count++操作,使用如下類實現(xiàn):AtomicInteger count = new AtomicInteger(); count.addAndGet(1); 如果是 JDK8,推薦使用 LongAdder 對象,比 AtomicLong 性能更好(減少樂觀鎖的重試次數(shù))。

18.【參考】HashMap 在容量不夠進行 resize 時由于高并發(fā)可能出現(xiàn)死鏈,導致 CPU 飆升,在開發(fā)過程中注意規(guī)避此風險。

19.【參考】ThreadLocal 對象使用 static 修飾,ThreadLocal 無法解決共享對象的更新問題。

  • 說明:這個變量是針對一個線程內(nèi)所有操作共享的,所以設置為靜態(tài)變量,所有此類實例共享此靜態(tài)變量,也就是說在類第一次被使用時裝載,只分配一塊存儲空間,所有此類的對象(只要是這個線程內(nèi)定義的)都可以操控這個變量。
以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號