java多線程編程模式

前言

區別於java設計模式,下面介紹的是在多線程場景下,如何設計出合理的思路。java

 

不可變對象模式

場景

1. 對象的變化頻率不高編程

每一次變化就是一次深拷貝,會影響cpu以及gc,若是頻繁操做會影響性能設計模式

2. 做爲hashmap的key數組

key若是是可變的,那麼會沒法從hashmap中找到原來的數據promise

3. 單線程寫,多線程讀或者遍歷等場景緩存

這種場景在讀或寫的任何操做都不須要加鎖,若是是多線程場景那麼在寫的時候須要加鎖。安全

 

思路

讓對象從初始化開始就不能被修改從而知足自然的線程安全條件,也就是說其餘任何操做都是讀操做,再也不有寫操做。當該對象遇到須要寫操做的場景時,再經過對其深拷貝的方式,建立出一個新的對象來代替。核心特徵有下面3個多線程

1. 類用final修飾併發

2. 全部字段用final修飾異步

3. 若是用到其餘可變的對象,那麼再對外提供對象時須要進行深拷貝。

 

JDK案例

CopyOnWriteArrayList

每一次寫操做都會深拷貝其內部的一個數組。只須要在寫的時候枷鎖,這是爲了防止多線程寫致使的併發問題,在讀取或者遍歷的時候不用加鎖。因此這個數據結果的場景是多讀少寫的場景。

 

保護性暫掛模式

場景

線程a想要執行一個操做,可是須要等待線程b完成另外一個操做

 

思路

抽象出中間類(下面用block代替)來保證線程安全和同步,將線程a須要執行的邏輯傳給block,block基於java的Lock和Condition實現通用的await和notify,線程b在操做完後調用block的釋放方法。說白了就是把await和notify提取出來,實現和對象無關的等待喚醒。

 

JDK案例

LinkedBlockingQueue

LinkedBlockingQueue採用了兩類鎖,put鎖和take鎖,也就是讀鎖和寫鎖。與之對應的衍生出了兩個Condition,這個隊列的特色是阻塞,當put的時候若是隊列滿了,那麼會阻塞直到隊列有空間,take操做也同樣,若是隊列沒數據則會一直等待直到有獲取到數據。

 

兩階段模式

場景

  1. 須要在優雅的關閉某個線程,好比某個sock正在循環監聽
  2. 須要在JVM結束前結束某個工做線程(與守護線程相對)

 

思路

所謂兩階段終止,就是把中止1個線程拆成兩步,第一步修改線程中的中止標誌位,常見的線程都是自循環的,改變標誌位意味着在這次邏輯後再也不進入下一循環;第二步是中斷線程,每一個線程都有本身的中斷邏輯,好比在wait的都notify了,在sleep的都interrupt了,從而達到快速中止的效果。

 

JDK案例

ThreadPoolExecutor

ThreadPoolExecutor.shutdown()的實現思路就是將狀態置爲SHUTDOWN,而後將沒有工做的線程直接中斷interrupt,最後等待正在工做的線程執行完最後一段邏輯。

 

承諾模式

場景

在保護性暫掛模式場景下,a線程須要b線程的執行結果,可是除此以外,a線程還須要其餘操做,也就是說須要兩個線程一塊兒執行。

 

思路

a線程先提交b線程,並獲取b線程的執行小票,等a線程執行完本身的邏輯後再根據執行小票獲取b線程的執行結果。

 

JDK案例

FutureTask

java自帶了promise的庫,能夠直接使用FutureTask類,再經過線程提交,從而達到異步效果。

 

生產者消費者模式

場景

生產者消費者模式多是咱們接觸的最多的模式了,好比事件分發,任務調度

 

思路

經過將生產者線程和消費者線程解耦,引入通道的概念,讓生產者把數據發到通道中,消費者再從通道中獲取數據

 

JDK案例

ThreadPoolExecutor

ThreadPoolExecutor的總體結構就是生產者和消費者,客戶端在submit任務或者execute任務的時候起到生產者的操做,當最大線程數到達閾值後,新進來的任務就會加入隊列,而ThreadPoolExecutor自己的構造函數就須要一個阻塞隊列,起到管道的做用,最後ThreadPoolExecutor內部有一個線程池來不斷的獲取管道的任務,從而執行任務。

 

主動對象模式

場景

這個模式的名稱聽起來可能有點抽象,其實就是抽象出一個對象來管理和維護異步任務執行,並對外提供任務提交等接口。對這聽起來就是一個線程池的功能。

 

思路

將異步任務的提交和執行解耦,構建一個專門維護全部異步任務的對象,當使用者須要執行異步任務,那麼能夠將異步任務提交給該對象,並快速返回,不用再關心任務的執行和調度。

JDK案例

ThreadPoolExecutor

ThreadPoolExecutor管理了一個線程池用於執行異步任務(這個模式不關心是線程仍是線程池,只是想表達有一個可以獨立維護管理異步任務執行的對象),並對外提供了submit和execute兩個提交任務的方法,這兩個方法原理同樣,只是submit會將Runnable對象封裝成FutureTask對象,從而能夠獲取返回值。當客戶端調用這兩個方法的時候,ThreadPoolExecutor會根據當前的線程數量,隊列空間來決定任務的執行,等待和拒絕,這些過程對客戶端來講都是無需等待的。

 

線程池模式

場景

須要週期性的去進行異步操做,要知道建立和銷燬線程的代價是很大的,因此須要對零散的線程進行統一管理。

 

思路

經過構建一個線程池列表,維護全部的線程。爲了知足不一樣的cpu資源使用場景須要,須要可以配置線程池的最大線程數最限制。爲了減小線程在空閒時間佔用的資源,須要可以配置對空閒線程的回收時間以及常駐線程數量大小。爲了提供異步任務排隊的概念,須要可以配置待執行任務的隊列。爲了能本身控制建立線程的屬性,須要可以配置線程構建工廠。爲了解決異步任務提交失敗的場景,須要可以配置任務提交的出錯策略。說了這麼多,其實就在說ThreadPoolExecutor的構造函數。

 

JDK案例

ThreadPoolExecutor

ThreadPoolExecutor是JDK1.5以後提供的一個線程池實現,強力推薦使用。下面列一個典型的構建函數實現。

// 建立一個
// 常駐線程數爲2,
// 最大線程數量上限爲10,
// 空閒線程過60s就回收,
// 任務等待隊列爲最大容量爲10的基於鏈表的阻塞隊列
// 線程的建立爲默認線程工廠,
// 任務提交失敗則拋出異常
// 線程池

ThreadPoolExecutor threadPoolExecutor
        = new ThreadPoolExecutor(
        2,
        10,
        60,
        TimeUnit.SECONDS,
        new LinkedBlockingQueue<Runnable>(20),
        Executors.defaultThreadFactory(),
        new ThreadPoolExecutor. AbortPolicy());

 

 

線程特有存儲模式

場景

在多線程場景下,某個對象須要被共享給多個線程,而且多個線程會對此對象進行修改和讀取操做,除此以外,共享的對象佔用空間很小,修改的頻率很高。最多見的就是利用線程本地存儲來共享一些環境配置。

 

思路

在高頻率的多線程修改場景下,須要儘量的避免鎖,不然線程之間會瘋狂競爭鎖致使性能降低。那麼將這個對象在每一個線程中都有一個拷貝是很好的選擇,每一個線程維護各自的對象,不須要加任何鎖。

 

JDK案例

ThreadLocal

ThreadLocal經過Thread中內置的ThreadMap來存儲數據,從而實現每一個線程擁有各自的對象。ThreadMap中用ThreadLocal做爲key,存儲的數據做爲value。須要注意的是,當該某個線程執行完以後,須要手動把該線程的數據remove,避免內存泄露。

提及來線程特有存儲模式和以前講到的不可變模式的思路有點像,只是前者緩存了對象,後者在須要用對象的時候從新深拷貝一個。能夠說是用空間換時間的操做。

 

串行線程封閉模式

把多個異步任務加入隊列,用單工做線程去執行,從而實現串行的效果。感受這個模式能夠簡單理解爲最大線程數是1的線程池,就很少說了。

 

主僕模式

 

思路

將一個複雜的單個任務拆成多個子任務,每一個子任務由不一樣的線程去執行,執行完後再彙總。這就造成了主僕模式

 

流水線模式

思路

能夠理解成串行封閉模式+主僕模式

 

半同步半異步模式

思路

對異步任務執行進行aop,意思就是說能夠自定義異步任務的執行前,執行後進行的相關邏輯,從而實現相關同步的操做。

 

總結

JDK提供了不少開箱即用的對象,特別是ThreadPoolExecutor,囊括了多種編程模式。

 

參考

《Java多線程編程實戰指南-設計模式篇》

相關文章
相關標籤/搜索