寫在前面的話:之前一直沒瞭解過多線程編程相關的東西,對於線程之類相關的概念也是一知半解。這次藉著準備面試的時機,計劃將一些關鍵性的盲區(除多線程以外,還有記憶體回收、圖片緩衝、效能最佳化等等)都徹底掃一遍。接下來我會陸續整理成學習筆記,主要作為自己學習後的溫習鞏固之用。如果能協助到同樣迷茫的小白的話,深感榮幸;如果因為缺少實戰導致理解有誤或解釋不清的地方,還請路過的大牛不吝賜教,共同進步^_^
知識點相關
作為這次學習的切入點,線程池關聯到的概念有很多,比如線程到底是什麼,常搭配使用的Runnable又是啥,為什麼又要搭配起來使用,等等。
關於這些,我找到了一篇博文:多線程(Thread、線程建立、線程池),裡面的解釋說明非常全面了,感興趣的可以整篇通讀,一定受益匪淺。我在這裡僅把幾個重要且基礎的點列出來,按自己的理解簡要說明一下:
- 線程Thread類:用於建立線程(start方法)和定義線程所執行的任務(run方法),這裡的run方法與主線程中的main方法等效。
- 任務Runnable介面:一般有兩種方法定義任務,一種是繼承Thread類重寫run方法,另一種則是實現Runnable介面重寫run方法。後者用的比較多,是常用搭配,目的就在於將線程建立和線程任務這兩塊解耦
- 任務Callable介面:作用與Runnable類似,call方法也與run方法地位相同,區別在於Callable返回結果而Runnable不返回
- 任務結果Future介面:對應的通過Future介面的get方法來擷取Callable返回的結果,注意:使用get方法時如還未得到結果則會阻塞當前線程直至拿到結果
- 任務FutureTask類:可以理解為是Runnable/Callable和Future的結合體,通過在Callable(Runnable也會先轉為Callable)外面加包一層,使用get方法直接擷取任務結果
除了以上這些涉及到的類,還有一些多線程相關的概念列舉如下:
- 進程:簡單來說,一個應用對應一個進程,佔用一定記憶體
- 線程:一個進程包含多個線程,線程相當於是任務執行單元,互不干擾
- 搶佔式調度:按優先順序高低分配向線程分配記憶體資源;多線程並不提高程式運行速度,而是在於提高運行效率,提高CPU的使用率
- 主線程:運行main方法所在的線程即為主線程;通常將耗時操作放到主線程以外的線程,以防阻塞主線程
準備工作完畢,下面重點講線程池↓
為什麼要使用線程池
簡單來說,線程池就像一個管家,通過調度和合理分配工作,讓每個傭人(即線程)在繁忙時都能各司其職高效完成任務,同時又能在空閑時得到充分休息。更具體來說,主要有兩點:一是合理利用現存的線程,減少線程建立和銷毀所消耗的資源開銷,加快響應;二是便於監督和管理每個線程的生命週期。
線程池ThreadPoolExecutor類
要搞清楚這個類,先上一張圖。
這是一張繼承關係圖。最初的Executor介面定義的是execute方法,比較簡單。它的子類ExecutorService介面,你可以理解為是通用線程池類,裡面增加了線程池常用的一些用於控制線程的方法,如submit和shutdown等,用於管理生命週期。繼承的AbstractExecutorService抽象類別,是在ExecutorService基礎上的預設實現。最後,ThreadPoolExecutor繼承自該抽象類別,通過構造方法來配置線程池。
這裡還要提一下Executors類,不在圖中,是線程池工廠類,可以通過一系列靜態方法建立線程池。查看源碼可以發現,方法內部都是使用ThreadPoolExecutor的構造方法加上不同的配置參數來實現。
下面是ThreadPoolExecutor類的幾個構造方法:
重點講第三個,其他的構造方法基本類似。其中的參數有(按順序依次):核心線程數N,最大線程數M,逾時時間T,時間單位U,緩衝隊列B和拒絕處理Handler。這些參數各自代表的含義,將結合不同的緩衝隊列類型分情況講解:
- SynchronousQueue類型:所有任務不進入緩衝隊列(可以理解為緩衝隊列的容量為0),直接進入可用線程執行,當無可用線程時則直接建立新線程執行任務。當線程數超過M時,調用Handler處理被拒絕的新任務。
- LinkedBlockingQueue類型:有新任務時,若當前線程數少於N,則建立新線程;若大於等於N,則進入緩衝隊列;當緩衝隊列充滿時,且線程數少於M,則繼續建立新線程,直至達到M時調用Handler處理拒絕任務。一般情況下,LinkedBlockingQueue作為無界隊列不指定緩衝容量,可以無限擴充,此時相當於M失效;但也可以通過建構函式來指定容量。
- ArrayBlockingQueue類型:規則與LinkedBlockingQueue類似,但一般情況下指定隊列長度。
具體這三種隊列類型的區別可以參考這篇文章:ThreadPoolExecutor的三種隊列
還有逾時時間T和時間單位U,主要作用在於令非核心線程閑置超過一定時間長度後自動銷毀,從而釋放資源。
拒絕任務Handler
當線程數和緩衝隊列都達到飽和時,會調用相應的Handler處理拒絕的任務。以下是四種預定義的處理策略:
- 在預設的AbortPolicy 中,拒絕後直接拋出異常RejectedExecutionException。
- 在CallerRunsPolicy 中,直接在 execute 方法的調用線程中運行被拒絕的任務。該策略提供簡單的反饋控制機制,能夠減緩新任務的提交速度。
- 在DiscardPolicy 中,拒絕後直接放棄任務。
- 在DiscardOldestPolicy 中,位於工作隊列頭部(即最舊)的任務將被放棄,然後重試執行程式(如果再次失敗,則重複此過程)。
線程池的簡單樣本
以斐波那契數列為例,主要展示了不同任務類型的用法區別,注意此處使用submit而不是execute來提交任務,兩者區別類似Callable和Runnable。
1 package com.effective.java.concurrent.task; 2 3 import java.util.concurrent.Callable; 4 import java.util.concurrent.ExecutionException; 5 import java.util.concurrent.ExecutorService; 6 import java.util.concurrent.Executors; 7 import java.util.concurrent.Future; 8 import java.util.concurrent.FutureTask; 9 10 public class RunnableFutureTask { 11 12 static ExecutorService mExecutor = new ThreadPoolExecutor(2, 3, 5, TimeUnit.SECONDS, new SynchronousQueue<Runnable>()); 13 14 15 public static void main(String[] args) { 16 futureDemo(); 17 } 18 19 20 static void futureDemo() { 21 try { 22 /** 23 * 提交runnable則沒有傳回值, future沒有資料 24 */ 25 Future<?> result = mExecutor.submit(new Runnable() { 26 27 @Override 28 public void run() { 29 fibc(20); 30 } 31 }); 32 33 System.out.println("future result from runnable : " + result.get()); 34 35 /** 36 * 提交Callable, 有傳回值, future中能夠擷取傳回值 37 */ 38 Future<Integer> result2 = mExecutor.submit(new Callable<Integer>() { 39 @Override 40 public Integer call() throws Exception { 41 return fibc(20); 42 } 43 }); 44 45 System.out 46 .println("future result from callable : " + result2.get()); 47 48 /** 49 * FutureTask可以通過Thread封裝來直接執行,也可以提交給ExecuteService來執行 50 * ,並且還可以通過v get()返回執行結果,線上程體沒有執行完成的時候,主線程一直阻塞等待,執行完則直接返回結果。 51 */ 52 FutureTask<Integer> futureTask = new FutureTask<Integer>( 53 new Callable<Integer>() { 54 @Override 55 public Integer call() throws Exception { 56 return fibc(20); 57 } 58 }); 59 60 // 提交futureTask 61 mExecutor.submit(futureTask) ; 62 System.out.println("future result from futureTask : " 63 + futureTask.get()); 64 65 } catch (InterruptedException e) { 66 e.printStackTrace(); 67 } catch (ExecutionException e) { 68 e.printStackTrace(); 69 } 70 } 71 72 /** 73 * 效率底下的斐波那契數列, 耗時的操作 74 * 75 * @param num 76 * @return 77 */ 78 static int fibc(int num) { 79 if (num == 0) { 80 return 0; 81 } 82 if (num == 1) { 83 return 1; 84 } 85 return fibc(num - 1) + fibc(num - 2); 86 } 87 }
除文中提到的博文外,其餘參考博文列舉如下,感謝各位博主的無私分享^_^:
java並發編程--Executor架構
一心多用多線程-線程池ThreadPoolExecutor-看這篇就夠了
一心多用多線程-細談java線程池submit與execute的區別
Java多線程-線程池ThreadPoolExecutor構造方法和規則
深入理解Callable
Java中的Runnable、Callable、Future、FutureTask的區別與樣本