併發處理規則最佳推薦

雲端計算頻道發表於2018-10-30

企業在架構設計時,都會遇到併發處理的問題。其實,解決併發問題,並不是難事,只要做到有章可循。以下是某網際網路企業的最佳實踐總結,僅供參考。

Rule 1. 【強制】建立執行緒或執行緒池時請指定有意義的執行緒名稱,方便出錯時回溯。

1)建立單條執行緒時直接指定執行緒名稱

2) 執行緒池則使用guava或自行封裝的ThreadFactory,指定命名規則。

Rule 2. 【推薦】儘量使用執行緒池來建立執行緒

除特殊情況,儘量不要自行建立執行緒,更好的保護執行緒資源。

同理,定時器也不要使用Timer,而應該使用ScheduledExecutorService。

因為Timer只有單執行緒,不能併發的執行多個在其中定義的任務,而且如果其中一個任務丟擲異常,整個Timer也會掛掉,而ScheduledExecutorService只有那個沒捕獲到異常的任務不再定時執行,其他任務不受影響。

Rule 3. 【強制】執行緒池不允許使用 Executors去建立,避資源耗盡風險

Executors返回的執行緒池物件的弊端 :

1)FixedThreadPool 和 SingleThreadPool:

允許的請求佇列長度為 Integer.MAX_VALUE,可能會堆積大量的請求,從而導致 OOM。

2)CachedThreadPool 和 ScheduledThreadPool:

允許的建立執行緒數量為 Integer.MAX_VALUE,可能會建立大量的執行緒,從而導致 OOM。

應透過 new ThreadPoolExecutor(xxx,xxx,xxx,xxx)這樣的方式,更加明確執行緒池的執行規則,合理設定Queue及執行緒池的core size和max size,建議使用vjkit封裝的ThreadPoolBuilder。

Rule 4. 【強制】正確停止執行緒

Thread.stop()不推薦使用,強行的退出太不安全,會導致邏輯不完整,操作不原子,已被定義成Deprecate方法。

停止單條執行緒,執行Thread.interrupt()。

停止執行緒池:

ExecutorService.shutdown(): 不允許提交新任務,等待當前任務及佇列中的任務全部執行完畢後退出;

ExecutorService.shutdownNow(): 透過Thread.interrupt()試圖停止所有正在執行的執行緒,並不再處理還在佇列中等待的任務。

最優雅的退出方式是先執行shutdown(),再執行shutdownNow(),vjkit的ThreadPoolUtil進行了封裝。

注意,Thread.interrupt()並不保證能中斷正在執行的執行緒,需編寫可中斷退出的Runnable,見規則5。

Rule 5. 【強制】編寫可停止的Runnable

執行Thread.interrupt()時,如果執行緒處於sleep(), wait(), join(), lock.lockInterruptibly()等blocking狀態,會丟擲InterruptedException,如果執行緒未處於上述狀態,則將執行緒狀態設為interrupted。

因此,如下的程式碼無法中斷執行緒:

5.1 正確處理InterruptException

因為InterruptException異常是個必須處理的Checked Exception,所以run()所呼叫的子函式很容易吃掉異常並簡單的處理成列印日誌,但這等於停止了中斷的傳遞,外層函式將收不到中斷請求,繼續原有迴圈或進入下一個堵塞。

正確處理是呼叫Thread.currentThread().interrupt(); 將中斷往外傳遞。

5.2 主迴圈及進入阻塞狀態前要判斷執行緒狀態

其他如Thread.sleep()的程式碼,在正式sleep前也會判斷執行緒狀態。

Rule 6. 【強制】Runnable中必須捕獲一切異常

如果Runnable中沒有捕獲RuntimeException而向外丟擲,會發生下列情況:

1) ScheduledExecutorService執行定時任務,任務會被中斷,該任務將不再定時排程,但執行緒池裡的執行緒還能用於其他任務。

2) ExecutorService執行任務,當前執行緒會中斷,執行緒池需要建立新的執行緒來響應後續任務。

3) 如果沒有在ThreadFactory設定自定義的UncaughtExceptionHanlder,則異常最終只列印在System.err,而不會列印在專案的日誌中。

因此建議自寫的Runnable都要保證捕獲異常; 如果是第三方的Runnable,可以將其再包裹一層vjkit中的SafeRunnable。

Rule 7. 【強制】全域性的非執行緒安全的物件可考慮使用ThreadLocal存放

全域性變數包括單例物件,static成員變數。

著名的非執行緒安全類包括SimpleDateFormat,MD5/SHA1的Digest。

對這些類,需要每次使用時建立。

但如果建立有一定成本,可以使用ThreadLocal存放並重用。

ThreadLocal變數需要定義成static,並在每次使用前重置。

Rule 8. 【推薦】縮短鎖

1) 能鎖區塊,就不要鎖整個方法體;

2)能用物件鎖,就不要用類鎖。

Rule 9. 【推薦】選擇分離鎖,分散鎖甚至無鎖的資料結構

分離鎖:

1) 讀寫分離鎖ReentrantReadWriteLock,讀讀之間不加鎖,僅在寫讀和寫寫之間加鎖;

2) Array Base的queue一般是全域性一把鎖,而Linked Base的queue一般是隊頭隊尾兩把鎖。

分散鎖(又稱分段鎖):

1)如JDK7的ConcurrentHashMap,分散成16把鎖;

2)對於經常寫,少量讀的計數器,推薦使用JDK8或vjkit封裝的LongAdder物件效能更好(內部分散成多個counter,減少樂觀鎖的使用,取值時再相加所有counter)

無鎖的資料結構:

1)完全無鎖無等待的結構,如JDK8的ConcurrentHashMap;

2)基於CAS的無鎖有等待的資料結構,如AtomicXXX系列。

Rule 10. 【推薦】基於ThreadLocal來避免鎖

比如Random例項雖然是執行緒安全的,但其實它的seed的訪問是有鎖保護的。因此建議使用JDK7的ThreadLocalRandom,透過在每個執行緒裡放一個seed來避免了加鎖。

Rule 11. 【推薦】規避死鎖風險

對多個資源多個物件的加鎖順序要一致。

如果無法確定完全避免死鎖,可以使用帶超時控制的tryLock語句加鎖。

Rule 12. 【推薦】volatile修飾符,AtomicXX系列的正確使用

多執行緒共享的物件,在單一執行緒內的修改並不保證對所有執行緒可見。使用volatile定義變數可以解決(解決了可見性)。

但是如果多條執行緒併發進行基於當前值的修改,如併發的counter++,volatile則無能為力(解決不了原子性)。

此時可使用Atomic*系列:

但如果需要原子地同時對多個AtomicXXX的Counter進行操作,則仍然需要使用synchronized將改動程式碼塊加鎖。

Rule 13. 【推薦】延時初始化的正確寫法

透過雙重檢查鎖(double-checked locking)實現延遲初始化存在隱患,需要將目標屬性宣告為volatile型,為了更高的效能,還要把volatile屬性賦予給臨時變數,寫法複雜。

所以如果只是想簡單的延遲初始化,可用下面的靜態類的做法,利用JDK本身的class載入機制保證唯一初始化。

 

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/31545808/viewspace-2218094/,如需轉載,請註明出處,否則將追究法律責任。

相關文章