前言
區別於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操作也一樣,如果隊列沒數據則會一直等待直到有獲取到數據。
兩階段模式
場景
- 需要在優雅的關閉某個線程,比如某個sock正在循環監聽
- 需要在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多線程編程實戰指南-設計模式篇》