區別於java設計模式,下面介紹的是在多線程場景下,如何設計出合理的思路。java
1. 對象的變化頻率不高編程
每一次變化就是一次深拷貝,會影響cpu以及gc,若是頻繁操做會影響性能設計模式
2. 做爲hashmap的key數組
key若是是可變的,那麼會沒法從hashmap中找到原來的數據promise
3. 單線程寫,多線程讀或者遍歷等場景緩存
這種場景在讀或寫的任何操做都不須要加鎖,若是是多線程場景那麼在寫的時候須要加鎖。安全
讓對象從初始化開始就不能被修改從而知足自然的線程安全條件,也就是說其餘任何操做都是讀操做,再也不有寫操做。當該對象遇到須要寫操做的場景時,再經過對其深拷貝的方式,建立出一個新的對象來代替。核心特徵有下面3個多線程
1. 類用final修飾併發
2. 全部字段用final修飾異步
3. 若是用到其餘可變的對象,那麼再對外提供對象時須要進行深拷貝。
每一次寫操做都會深拷貝其內部的一個數組。只須要在寫的時候枷鎖,這是爲了防止多線程寫致使的併發問題,在讀取或者遍歷的時候不用加鎖。因此這個數據結果的場景是多讀少寫的場景。
線程a想要執行一個操做,可是須要等待線程b完成另外一個操做
抽象出中間類(下面用block代替)來保證線程安全和同步,將線程a須要執行的邏輯傳給block,block基於java的Lock和Condition實現通用的await和notify,線程b在操做完後調用block的釋放方法。說白了就是把await和notify提取出來,實現和對象無關的等待喚醒。
LinkedBlockingQueue採用了兩類鎖,put鎖和take鎖,也就是讀鎖和寫鎖。與之對應的衍生出了兩個Condition,這個隊列的特色是阻塞,當put的時候若是隊列滿了,那麼會阻塞直到隊列有空間,take操做也同樣,若是隊列沒數據則會一直等待直到有獲取到數據。
所謂兩階段終止,就是把中止1個線程拆成兩步,第一步修改線程中的中止標誌位,常見的線程都是自循環的,改變標誌位意味着在這次邏輯後再也不進入下一循環;第二步是中斷線程,每一個線程都有本身的中斷邏輯,好比在wait的都notify了,在sleep的都interrupt了,從而達到快速中止的效果。
ThreadPoolExecutor.shutdown()的實現思路就是將狀態置爲SHUTDOWN,而後將沒有工做的線程直接中斷interrupt,最後等待正在工做的線程執行完最後一段邏輯。
在保護性暫掛模式場景下,a線程須要b線程的執行結果,可是除此以外,a線程還須要其餘操做,也就是說須要兩個線程一塊兒執行。
a線程先提交b線程,並獲取b線程的執行小票,等a線程執行完本身的邏輯後再根據執行小票獲取b線程的執行結果。
java自帶了promise的庫,能夠直接使用FutureTask類,再經過線程提交,從而達到異步效果。
生產者消費者模式多是咱們接觸的最多的模式了,好比事件分發,任務調度
經過將生產者線程和消費者線程解耦,引入通道的概念,讓生產者把數據發到通道中,消費者再從通道中獲取數據
ThreadPoolExecutor的總體結構就是生產者和消費者,客戶端在submit任務或者execute任務的時候起到生產者的操做,當最大線程數到達閾值後,新進來的任務就會加入隊列,而ThreadPoolExecutor自己的構造函數就須要一個阻塞隊列,起到管道的做用,最後ThreadPoolExecutor內部有一個線程池來不斷的獲取管道的任務,從而執行任務。
這個模式的名稱聽起來可能有點抽象,其實就是抽象出一個對象來管理和維護異步任務執行,並對外提供任務提交等接口。對這聽起來就是一個線程池的功能。
將異步任務的提交和執行解耦,構建一個專門維護全部異步任務的對象,當使用者須要執行異步任務,那麼能夠將異步任務提交給該對象,並快速返回,不用再關心任務的執行和調度。
ThreadPoolExecutor管理了一個線程池用於執行異步任務(這個模式不關心是線程仍是線程池,只是想表達有一個可以獨立維護管理異步任務執行的對象),並對外提供了submit和execute兩個提交任務的方法,這兩個方法原理同樣,只是submit會將Runnable對象封裝成FutureTask對象,從而能夠獲取返回值。當客戶端調用這兩個方法的時候,ThreadPoolExecutor會根據當前的線程數量,隊列空間來決定任務的執行,等待和拒絕,這些過程對客戶端來講都是無需等待的。
須要週期性的去進行異步操做,要知道建立和銷燬線程的代價是很大的,因此須要對零散的線程進行統一管理。
經過構建一個線程池列表,維護全部的線程。爲了知足不一樣的cpu資源使用場景須要,須要可以配置線程池的最大線程數最限制。爲了減小線程在空閒時間佔用的資源,須要可以配置對空閒線程的回收時間以及常駐線程數量大小。爲了提供異步任務排隊的概念,須要可以配置待執行任務的隊列。爲了能本身控制建立線程的屬性,須要可以配置線程構建工廠。爲了解決異步任務提交失敗的場景,須要可以配置任務提交的出錯策略。說了這麼多,其實就在說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());
在多線程場景下,某個對象須要被共享給多個線程,而且多個線程會對此對象進行修改和讀取操做,除此以外,共享的對象佔用空間很小,修改的頻率很高。最多見的就是利用線程本地存儲來共享一些環境配置。
在高頻率的多線程修改場景下,須要儘量的避免鎖,不然線程之間會瘋狂競爭鎖致使性能降低。那麼將這個對象在每一個線程中都有一個拷貝是很好的選擇,每一個線程維護各自的對象,不須要加任何鎖。
ThreadLocal經過Thread中內置的ThreadMap來存儲數據,從而實現每一個線程擁有各自的對象。ThreadMap中用ThreadLocal做爲key,存儲的數據做爲value。須要注意的是,當該某個線程執行完以後,須要手動把該線程的數據remove,避免內存泄露。
提及來線程特有存儲模式和以前講到的不可變模式的思路有點像,只是前者緩存了對象,後者在須要用對象的時候從新深拷貝一個。能夠說是用空間換時間的操做。
把多個異步任務加入隊列,用單工做線程去執行,從而實現串行的效果。感受這個模式能夠簡單理解爲最大線程數是1的線程池,就很少說了。
將一個複雜的單個任務拆成多個子任務,每一個子任務由不一樣的線程去執行,執行完後再彙總。這就造成了主僕模式
能夠理解成串行封閉模式+主僕模式
對異步任務執行進行aop,意思就是說能夠自定義異步任務的執行前,執行後進行的相關邏輯,從而實現相關同步的操做。
JDK提供了不少開箱即用的對象,特別是ThreadPoolExecutor,囊括了多種編程模式。
《Java多線程編程實戰指南-設計模式篇》