Java線程安全策略與多線程併發最佳實踐

 

線程安全策略

不可變對象java

不可變對象(Immutable Objects)是指對象一旦被建立它的狀態(對象的數據,也即對象屬性值)就不能改變,任何對它的改變都應該產生一個新的對象。spring

不可變對象須要知足的條件:數據庫

  1. 對象建立之後其狀態就不能修改
  2. 對象全部域都是final類型
  3. 對象時正確建立的(在對象建立期間,this引用沒有逸出)

除了使用final自行封裝不可變對象以外,還能夠經過如下兩種方式定義不可變對象api

  • Collections.unmodifiableXXX():XXX能夠是Collection、List、Set、Map。使用api將已有普通集合類對象轉變成不可變對象。原理是進行包裝,而後定義全部修改函數拋出異常。
  • guava裏面的ImmutableXXX。XXX能夠是Collection、List、Set、Map。

線程封閉數組

當訪問共享的可變數據時,一般須要同步。一種避免同步的方式就是不共享數據。若是僅在單線程內訪問數據,就不須要同步,這種技術稱爲線程封閉。安全

常見線程封閉手段:數據結構

  • 堆棧封閉:局部變量,沒有併發問題
  • threadlocal:特別好的線程封閉方法。一般在filter將用戶信息保存threadlocal。

spring中必定要在攔截器afterCompletion中,執行threadlocal的remove函數,線程池中使用同理。多線程

同步容器架構

stringbuilder:線程不安全(能夠在函數中定義,利用堆棧封閉避免了線程不安全,同時節省了加鎖的消耗,性能更好)併發

stringbuffer:線程安全(每一個函數都是用synchronized修飾),能夠作全局變量。

SimpleDateFormat:JDK中的工具類,線程不安全。使用方法能夠參考stringbuilder。

JodaTime:線程安全,功能更豐富。

ArrayList/HashSet/HashMap等Collections:都是線程不安全的

Vector/Stack/HashTable:都是線程安全的

先檢查再執行:if(condition(a)){handle(a)},這種形式若是沒有加鎖的話,就不是原子性,也是線程不安全的

併發容器

線程安全的容器除了上文提到的同步容器一些外,在Java的J.U.C(java.utils.concurrent的縮寫)下,一樣提供了線程安全的併發容器。

  1. CopyOnWriteArrayList
  2. 對應ArrayList,是線程安全容器。適合讀多寫少的場景(讀不加鎖,寫加可重入鎖。讀是讀的原數組,寫是在新數組)
  3. 缺點:
  4. 消耗內存,可能引起gc
  5. 不能用於實時讀
  6. CopyOnWriteArraySet/ConcurrentSkipListSet
  7. 對應HashSet/TreeSet,是線程安全容器。其中CopyOnWriteArraySet底層是CopyOnWriteArrayList
  8. ConcurrentHashMap/ConcurentSkipListMap
  9. 對應HashMap/TreeMap,是線程安全容器。ConcurentSkipListMap支持key有序,並且後者支持更高的併發數,由於它的存取時間和線程數是沒有關係的。

注意:併發容器的批量操做都不是線程安全的,例如調用removeAll,containsAll等,須要自行加鎖。

CopyOnWriteArrayList、CopyOnWriteArraySet,這種利用cow特性的數據結構,須要copy消耗內存,可能引起gc。

想免費學習Java工程化、分佈式架構、高併發、高性能、深刻淺出、微服務架構、SpringMyBatisNetty源碼分析等技術的朋友,能夠加羣:834962734,羣裏有阿里大牛直播講解技術,以及Java大型互聯網技術的視頻免費分享給你們,歡迎進羣一塊兒深刻交流學習無論你是轉行,仍是工做中想提高本身能力均可以

死鎖

線程死鎖是指因爲兩個或者多個線程互相持有對方所須要的資源,致使這些線程處於等待狀態,沒法前往執行。

死鎖的必要條件

  1. 互斥條件。進程對於所分配到的資源具備排它性,即一個資源只能被一個進程佔用,直到被該進程釋放
  2. 請求和保持條件。一個進程因請求被佔用資源而發生阻塞時,對已得到的資源保持不放。
  3. 不剝奪條件。任何一個資源在沒被該進程釋放以前,任何其餘進程都沒法對他剝奪佔用。
  4. 環路等待條件。當發生死鎖時,所等待的進程一定會造成一個環路(相似於死循環),形成永久阻塞。

死鎖示例代碼:

 
@Slf4j public class DeadLock implements Runnable { public int flag = 1; //靜態對象是類的全部對象共享的 private static Object o1 = new Object(), o2 = new Object(); @Override public void run() { log.info("flag:{}", flag); if (flag == 1) { synchronized (o1) { try { Thread.sleep(500); } catch (Exception e) { e.printStackTrace(); } synchronized (o2) { log.info("1"); } } } if (flag == 0) { synchronized (o2) { try { Thread.sleep(500); } catch (Exception e) { e.printStackTrace(); } synchronized (o1) { log.info("0"); } } } } public static void main(String[] args) { DeadLock td1 = new DeadLock(); DeadLock td2 = new DeadLock(); td1.flag = 1; td2.flag = 0; //td1,td2都處於可執行狀態,但JVM線程調度先執行哪一個線程是不肯定的。 //td2的run()可能在td1的run()以前運行 new Thread(td1).start(); new Thread(td2).start(); } }

避免死鎖的方法

  1. 注意加鎖順序。當多個線程須要相同的一些鎖,可是按照不一樣的順序加鎖,死鎖就很容易發生。若是能確保全部的線程都是按照相同的順序得到鎖,那麼死鎖就不會發生。
  2. 加鎖有時限。在嘗試獲取鎖的時候加一個超時時間,這也就意味着在嘗試獲取鎖的過程當中若超過了這個時限該線程則放棄對該鎖請求。
  3. 死鎖檢測。主要是針對那些不可能實現按序加鎖而且鎖超時也不可行的場景。每當一個線程得到了鎖,會在線程和鎖相關的數據結構中(map、graph等等)將其記下。除此以外,每當有線程請求鎖,也須要記錄在這個數據結構中。當一個線程請求鎖失敗時,這個線程能夠遍歷鎖的關係圖看看是否有死鎖發生。

死鎖排查方法

雖然形成死鎖的緣由是由於咱們設計得不夠好,可是可能寫代碼的時候不知道哪裏發生了死鎖。

JDK提供了兩種方式來給咱們檢測:

  • JconsoleJDK自帶的圖形化界面工具,使用JDK給咱們的的工具JConsole
  • Jstack是JDK自帶的命令行工具,主要用於線程Dump分析。

檢測出死鎖時的解決方案

一個可行的作法是釋放全部鎖,回退,而且等待一段隨機的時間後重試。這個和簡單的加鎖超時相似,不同的是隻有死鎖已經發生了纔回退,而不會是由於加鎖的請求超時了。雖然有回退和等待,可是若是有大量的線程競爭同一批鎖,它們仍是會重複地死鎖(編者注:緣由同超時相似,不能從根本上減輕競爭)。

一個更好的方案是給這些線程設置優先級,讓一個(或幾個)線程回退,剩下的線程就像沒發生死鎖同樣繼續保持着它們須要的鎖。若是賦予這些線程的優先級是固定不變的,同一批線程老是會擁有更高的優先級。爲避免這個問題,能夠在死鎖發生的時候設置隨機的優先級。

多線程併發最佳實踐

1. 使用本地變量

儘可能使用本地變量,而不是建立一個類或實例的變量。

 
class concurrentTask { private static List temp = new ArrayList<>(); public void execute(Message message) { // 使用本地變量保證線程安全 // List temp = new ArrayList<>(); temp.add(message.getId()); temp.add(message.getCode()); // ...省略各類業務邏輯 temp.clear(); } }

2. 使用不可變類

不可變類好比String 、Integer等一旦建立,再也不改變,不可變類能夠下降代碼中須要的同步數量。

3. 最小化鎖的做用域範圍

"阿姆達爾定律",又稱"安達爾定理": S=1/(1-a+a/n)

a:並行計算部分所佔比例

n:並行處理結點個數

S:加速比

當1-a等於0時,沒有串行只有並行,最大加速比 S=n

當a=0時,只有串行沒有並行,最小加速比 S = 1

當n→∞時,極限加速比 s→ 1/(1-a)

例如,若串行代碼佔整個代碼的25%,則並行處理的整體性能不可能超過4。

4. 使用線程池,而不是直接使用new Thread執行

避免new Thread建立線程。經過線程池的管理,可提升線程的複用性(避免新建線程的昂貴的資源消耗),簡化線程生命週期的管理。JDK提供了各類ThreadPool線程池和Executor。

5. 寧肯使用同步工具類也不要使用線程的wait和notify

同步工具類包括:countdownlaunch/Semaphore/Semaphore。應當優先使用這些同步工具,而不是去思考如何使用線程的wait和notify。此外,使用BlockingQueue實現生產消費的設計比使用wait和notify要好。

6. 使用blockingqueue實現生產消費模式

阻塞隊列是生產者-消費者模式的最好的實現方式,不只包括單個生產者單個消費者,還支持多個生產者多個消費者狀況。

7. 使用併發集合而不是加了鎖的同步集合

JDK提供了ConcurrentHashMap、CopyOnWriteArrayList、CopyOnWriteArraySet、BlockingQueue中的Deque和BlockingDeque五大併發集合,他們有着較好性能;儘可能使用該併發集合,而避免使用synchronizedXXX的鎖同步集合。

8. 使用semaphore建立有界的訪問

爲了創建穩定可靠的系統,對於數據庫、文件系統和socket等資源必需要作有界的訪問,Semaphone能夠限制這些資源開銷的選擇,Semaphone能夠以最低的代價阻塞線程等待,能夠經過Semaphone來控制同時訪問指定資源的線程數。

9. 寧肯使用同步代碼塊,也不使用同步的方法

主要針對synchronized關鍵字。使用synchronized關鍵字同步代碼塊只會鎖定一個對象,而不會將整個方法鎖定(當類不是單例的時候)。若是更改共同的變量或類的字段,首先應該選擇的是原子型變量,而後使用volatile。若是須要互斥鎖,能夠考慮使用ReentrantLock。

10. 避免使用靜態變量

靜態變量在多線程併發環境中會形成較多的問題。當使用靜態變量時,優先將其指定爲final變量,若用其來保存集合Collection變量,則考慮使用只讀集合。詳見上文的不可變對象,同步容器和併發容器。

咱們老是想得太多,作的太少!

相關文章
相關標籤/搜索