java多線程編程模式


前言

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

 

不可變對象模式

場景

1. 對象的變化頻率不高

每一次變化就是一次深拷貝,會影響cpu以及gc,如果頻繁操作會影響性能

2. 作為hashmap的key

key如果是可變的,那么會無法從hashmap中找到原來的數據

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多線程編程實戰指南-設計模式篇》


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM