1. 程式人生 > >java多執行緒程式設計模式

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