阿里編程規(guī)約之并發(fā)處理

本來是不打算談并發(fā)處理的,原因有二,一是因為大家在日常開發(fā)自用應用時,流量很少,并發(fā)的需求基本不會出現。二是因為我是不建議大家以一種試試的心態(tài)去做并發(fā)處理,因為并發(fā)不同于同步,很多時候如果你對自己所使用的api真正的用法不清楚的話,貿然使用,大概率會造成隱性bug。而且這種問題往往不能立即驗證出來,尤其是在大家還沒掌握單元測試,我們也沒有性能測試的情況下。我之前也提過,最麻煩的問題不是報錯了,而是程序給的結果異常,卻沒有報錯。甚至不是每次都能出現。這樣的問題,往往可能和你寫的并發(fā)處理不周全有關。綜上所述,在目前的應用開發(fā)中,建議大家不是必須,不要隨便嘗試寫并發(fā)處理的代碼。
當然,既然還是忍不住談及并發(fā)處理了,我也簡單說下我對于并發(fā)的認識吧。首先我們知道,并發(fā)的對象是線程。在java里,代表線程的類是thread。還有一個runnable接口,也可以啟動一個線程。線程是有生命周期的,也就是它可以進行的基本操作。線程也分優(yōu)先級Priority。還有一個特殊的線程-守護線程Daemon。我們可以把一類線程放到線程組ThreadGroup里。 如果我們想要復用線程呢,就可以使用線程池ThreadPool。這些只是線程本身涉及到知識點。如果想實現線程并發(fā),就不免要處理好臨界區(qū),即要保證線程安全。線程安全是并發(fā)操作的基本要求。這時就要談到最重要的同步方法-鎖了。我們最開始認識到sychronizd關鍵字就是一類典型的悲觀鎖。java后續(xù)也提供lock這樣的鎖??梢灾С治覀冊诟鄳脠鼍跋赂`活的使用鎖。與悲觀鎖對應就是樂觀鎖。你也可以稱之無鎖。它所依賴的CAS(Compare and Swap)原子操作技術,就是一種樂觀的不斷嘗試的操作。作為java程序員,我們平時最有可能接觸到的是建立在鎖或者原子操作上的java的并發(fā)包了。其中,并發(fā)容器是非常需要利用起來的工具。如果你對并發(fā)的操作很感興趣,研究下java在java.util.concurrent包的api。研究下它是怎么保證不同的集合并發(fā)時線程安全的。保證可以讓你受益匪淺。保證線程安全,除了保證多線程對于臨界區(qū)資源的同步操作外,其實還有一種解決思路。它的代表就是ThreadLocal。即通過給每個線程建立一個資源副本,來滿足線程對于資源的消費。因為這樣的局部資源只能在當前線程中訪問,自然是線程安全的。還有我們要知道volatile并不能保證線程安全,它只能保證資源可見。
到這里,我們基本涵蓋了java對于并發(fā)操作所作的基礎支持了。沒錯,以上這些只是基礎而已。然后就算我們掌握了上面的所有知識,我們也不能上來就拿著錘子去敲我們看到所有的東西。你有可能敲對,但是大部分情況下都只是破壞。做好并發(fā)最后也是最重要的是要遵循一些方法論。而這些方法論后來就形成了多個并發(fā)模式。比較有名的有future和callable,fork-join等。我對之也是知之甚少的。這里就不多介紹啦。
由此可見,java應用如果想具備高性能的并發(fā)能力,要求開發(fā)者掌握的東西是比較多的。所以當你還沒對并發(fā)知識積累到一定程度時,盡量不要在編碼中貿然使用相關內容吧。對于開發(fā)手冊里的關于并發(fā)處理的要求,請見如下。

1. 【強制】 獲取單例對象需要保證線程安全,其中的方法也要保證線程安全。
說明: 資源驅動類、工具類、單例工廠類都需要注意。
2. 【強制】創(chuàng)建線程或線程池時請指定有意義的線程名稱,方便出錯時回溯。
正例:
public class TimerTaskThread extends Thread {
public TimerTaskThread() {
super.setName("TimerTaskThread");
...
}
3. 【強制】線程資源必須通過線程池提供,不允許在應用中自行顯式創(chuàng)建線程。
說明: 使用線程池的好處是減少在創(chuàng)建和銷毀線程上所花的時間以及系統(tǒng)資源的開銷,解決資
源不足的問題。如果不使用線程池,有可能造成系統(tǒng)創(chuàng)建大量同類線程而導致消耗完內存或者
“過度切換”的問題。
4. 【強制】線程池不允許使用 Executors 去創(chuàng)建,而是通過 ThreadPoolExecutor 的方式,這樣
的處理方式讓寫的同學更加明確線程池的運行規(guī)則,規(guī)避資源耗盡的風險。
說明: Executors 返回的線程池對象的弊端如下:
1) FixedThreadPool 和 SingleThreadPool:
允許的請求隊列長度為 Integer.MAX_VALUE,可能會堆積大量的請求,從而導致 OOM。
2) CachedThreadPool 和 ScheduledThreadPool:
允許的創(chuàng)建線程數量為 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. 【強制】高并發(fā)時,同步調用應該去考量鎖的性能損耗。能用無鎖數據結構,就不要用鎖; 能
鎖區(qū)塊,就不要鎖整個方法體; 能用對象鎖,就不要用類鎖。
說明: 盡可能使加鎖的代碼塊工作量盡可能的小,避免在鎖代碼塊中調用 RPC 方法。
7. 【強制】對多個資源、數據庫表、對象同時加鎖時,需要保持一致的加鎖順序,否則可能會造
成死鎖。
說明: 線程一需要對表 A、 B、 C 依次全部加鎖后才可以進行更新操作,那么線程二的加鎖順序
也必須是 A、 B、 C,否則可能出現死鎖。
8. 【強制】并發(fā)修改同一記錄時,避免更新丟失, 需要加鎖。 要么在應用層加鎖,要么在緩存加
鎖,要么在數據庫層使用樂觀鎖,使用 version 作為更新依據。
說明: 如果每次訪問沖突概率小于 20%,推薦使用樂觀鎖,否則使用悲觀鎖。樂觀鎖的重試次
數不得小于 3 次。
9. 【強制】多線程并行處理定時任務時, Timer 運行多個 TimeTask 時,只要其中之一沒有捕獲
拋出的異常,其它任務便會自動終止運行,使用 ScheduledExecutorService 則沒有這個問題。
10. 【推薦】使用 CountDownLatch 進行異步轉同步操作,每個線程退出前必須調用 countDown
方法,線程執(zhí)行代碼注意 catch 異常,確保 countDown 方法被執(zhí)行到,避免主線程無法執(zhí)行
至 await 方法,直到超時才返回結果。
說明: 注意,子線程拋出異常堆棧,不能在主線程 try-catch 到。
11. 【推薦】避免 Random 實例被多線程使用,雖然共享該實例是線程安全的,但會因競爭同一
seed 導致的性能下降。
說明: Random 實例包括 java.util.Random 的實例或者 Math.random()的方式。
正例: 在 JDK7 之后,可以直接使用 API ThreadLocalRandom, 而在 JDK7 之前, 需要編碼保
證每個線程持有一個實例。
12. 【推薦】 在并發(fā)場景下, 通過雙重檢查鎖(double-checked locking) 實現延遲初始化的優(yōu)
化問題隱患(可參考 The "Double-Checked Locking is Broken" Declaration), 推薦解
決方案中較為簡單一種(適用于 JDK5 及以上版本) ,將目標屬性聲明為 volatile 型。
反例:
class Singleton {
private Helper helper = null;
public Helper getHelper() {
if (helper == null) synchronized(this) {
if (helper == null)
helper = new Helper();
}
return helper;
}
// other methods and fields...
}
13. 【參考】 volatile 解決多線程內存不可見問題。對于一寫多讀,是可以解決變量同步問題,
但是如果多寫,同樣無法解決線程安全問題。如果是 count++操作,使用如下類實現:
AtomicInteger count = new AtomicInteger(); count.addAndGet(1); 如果是 JDK8,推
薦使用 LongAdder 對象,比 AtomicLong 性能更好(減少樂觀鎖的重試次數) 。
14. 【參考】 HashMap 在容量不夠進行 resize 時由于高并發(fā)可能出現死鏈,導致 CPU 飆升,在
開發(fā)過程中可以使用其它數據結構或加鎖來規(guī)避此風險。
15. 【參考】 ThreadLocal 無法解決共享對象的更新問題, ThreadLocal 對象建議使用 static
修飾。這個變量是針對一個線程內所有操作共享的,所以設置為靜態(tài)變量,所有此類實例共享
此靜態(tài)變量 ,也就是說在類第一次被使用時裝載,只分配一塊存儲空間,所有此類的對象(只
要是這個線程內定義的)都可以操控這個變量。
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容