阿里巴巴Java開發手冊:編程規約.併發處理

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

說明:jstack(Stack Trace for Java)命令用於生成虛擬機當前時刻的線程快照。線程快照就是當前虛擬機內每一條線程正在執行的方法堆棧的集合.

生成線程快照的目的主要是定位線程長時間出現停頓的原因,如線程間死鎖、死循環、請求外部資源導致的長時間等待等都是導致線程長時間停頓的原因。

指定線程名稱後,可以在線程快照裏看到是哪些線程受到阻礙,及時定位到出現問題的代碼位置,進行錯誤修復。

3.【強制】線程資源必須通過線程池提供,不允許在應用中自行顯式創建線程。

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

《Java併發編程的藝術》中提到,使用線程池可以:降低資源消耗;提高響應速度;提高線程的可管理性.

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

說明:Executors返回的線程池對象的弊端如下:

1) FixedThreadPool和SingleThreadPool:
允許的請求隊列長度爲Integer.MAX_VALUE,可能會堆積大量的請求,從而導致OOM。

2) CachedThreadPool: 允許的創建線程數量爲Integer.MAX_VALUE,可能會創建大量的線程,從而導致OOM。

5.【強制】SimpleDateFormat 是線程不安全的類,一般不要定義爲static變量,如果定義爲static,必須加鎖,或者使用DateUtils工具類。

正例:注意線程安全,使用DateUtils。亦推薦如下使用ThreadLocal,確保每個線程都可以得到單獨的一個 SimpleDateFormat 的對象,自然也就不存在競爭問題了:

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變量,尤其在線程池場景下,線程經常會被複用,如果不清理自定義的 ThreadLocal變量,可能會影響後續業務邏輯和造成內存泄露等問題。儘量在代理中使用try-finally塊進行回收。

正例:

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

說明:ThreadLocalMap中使用的key爲ThreadLocal的弱引用,而 value是強引用。如果ThreadLocal沒有被外部強引用,在垃圾回收的時候,key會被清理掉,而value不會被清理掉。這樣一來,ThreadLocalMap中就會出現key爲null的Entry。

假如我們不做任何措施的話,value 永遠無法被GC回收,這個時候就可能會產生內存泄露。ThreadLocalMap實現中已經考慮了這種情況,在調用set()、get()、remove() 方法的時候,會清理掉key爲null的記錄。使用完ThreadLocal方法後 最好手動調用remove()方法。

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

說明:線程一需要對錶A、B、C依次全部加鎖後纔可以進行更新操作,那麼線程二的加鎖順序也必須是A、B、C,否則可能出現死鎖。

死鎖產生的四個條件:

互斥條件:該資源任意一個時刻只由一個線程佔用。該條件無法破壞,因爲我們就是需要產生互斥。

請求與保持條件:一個進程因請求資源而阻塞時,對已獲得的資源保持不放。通過一次性申請所有資源來破壞。

不剝奪條件:線程已獲得的資源在末使用完之前不能被其他線程強行剝奪,只有自己使用完畢後才釋放資源。通過線程申請資源失敗後主動釋放資源來破壞。

循環等待條件:若干進程之間形成一種頭尾相接的循環等待資源關係。通過按序申請資源來破壞。按某一順序申請資源,釋放資源則反序釋放。

該要求就是通過破壞循環等待條件來預防死鎖產生。

9.【強制】在使用阻塞等待獲取鎖的方式中,必須在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();
}

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

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

正例:

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

11.【強制】併發修改同一記錄時,避免更新丟失,需要加鎖。要麼在應用層加鎖,要麼在緩存加鎖,要麼在數據庫層使用樂觀鎖,使用version作爲更新依據。

說明:如果每次訪問衝突概率小於20%,推薦使用樂觀鎖,否則使用悲觀鎖。樂觀鎖的重試次數不得小於3次。樂觀鎖適合讀多寫少,衝突少的情況,悲觀鎖相反。

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

說明:ScheduledThreadPoolExecutor主要用來在給定的延遲後運行任務,或者定期執行任務。ScheduledThreadPoolExecutor使用的任務隊列DelayQueue中封裝了一個PriorityQueue,PriorityQueue會對隊列中的任務進行排序。

執行所需時間短的放在前面先被執行(ScheduledFutureTask的time變量小的先執行),如果執行所需時間相同則先提交的任務將被先執行(ScheduledFutureTask的squenceNumber 變量小的先執行)。

ScheduledThreadPoolExecutor和Timer的比較:

1)Timer 對系統時鐘的變化敏感,ScheduledThreadPoolExecutor不敏感;

2)Timer 只有一個執行線程,因此長時間運行的任務可以延遲其他任務。 ScheduledThreadPoolExecutor 可以配置任意數量的線程。

3)在TimerTask 中拋出的運行時異常會殺死一個線程,從而導致 Timer 死機,即計劃任務將不再運行。ScheduledThreadExecutor 不僅捕獲運行時異常,還允許在需要時處理它們(通過重寫 afterExecute 方法ThreadPoolExecutor)。拋出異常的任務將被取消,但其他任務將繼續運行。

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

說明:如果是count++操作,使用如下原子類實現:AtomicInteger count = new AtomicInteger(); count.addAndGet(1);

如果是JDK8,推薦使用LongAdder對象,比AtomicLong性能更好(減少樂觀鎖的重試次數)。

volatile只能保證變量的可見性,不能保證原子性。

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

說明:HashMap不是線程安全的,要保證線程安全可以使用ConcurrentHashMap。出現死鏈的原因主要是jdk1.7中HashMap擴容時使用頭插法。

具體可見:https://blog.csdn.net/swpu_ocean/article/details/88917958

總結

這部分主要是高併發項目的一些要求和說明,涉及線程池,鎖,AQS等,推薦學習《Java併發編程的藝術》和《Java併發編程實戰》。

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