Java開發筆記(一百零四)普通線程池的運用

前面介紹了線程的基本用法,以及多線程併發的問題處理,但實際開發中每每存在許多性質類似的任務,好比批量發送消息、批量下載文件、批量進行交易等等。這些同類任務的處理流程一致,不存在資源共享問題,相互之間也不須要通訊交互,總之每一個任務均可以看做是單獨的事務,彷彿流水線上的原材料通過一系列步驟加工以後變爲成品。可要是開啓分線程的話,得對每項任務都分別建立新線程並予以啓動,且不說如何的費時費力,單說這批量操做有多少任務就要開啓多少分線程,系統的有限資源禁不起這麼多的線程同時過來折騰。
就像工廠裏的流水線,每條流水線的生產速度是有限的,一會兒涌來大量原材料,一條流水線也消化不了,得多開幾條流水線才行。可是流水線也不能想開就開,畢竟每開一條流水線都要佔用工廠地盤,並且流水線開多了的話,後續沒有這麼多原材料的時候,豈不是形成資源浪費?到時又得關閉多餘的流水線,純屬人傻錢多瞎折騰。因此呢,合理的作法應當是先開少數幾條流水線,假若有大批來料須要加工,再多開幾條流水線,並且這些流水線要進行統一調度管理,新加的原料得放到空閒的流水線上加工,而不是再開新的流水線,這樣才能在最大程度上節約生產資源、提升工做效率。
Java體系之中,若將線程比做流水線的話,好幾個常駐的運行線程便組成了批量處理的工廠,那麼工廠裏面統一管理這些流水線的調度中心則被稱爲「線程池」。線程池封裝了線程的建立、啓動、關閉等操做,以及系統的資源分配與線程調度;它還支持任務的添加和移除功能,使得程序員能夠專心編寫任務代碼的業務邏輯,沒必要操心線程怎麼跑這些細枝末節。Java提供的線程池工具最經常使用的是ExecutorService及其派生類ThreadPoolExecutor,它支持如下四種線程池類型:
一、只有一個線程的線程池,該線程池由Executors類的newSingleThreadExecutor方法建立而來。它的建立代碼示例以下:html

		// 建立一個只有一個線程的線程池
		ExecutorService pool = (ExecutorService) Executors.newSingleThreadExecutor();

 

二、擁有固定數量線程的線程池,該線程池由Executors類的newFixedThreadPool方法建立而來,方法參數即爲線程數量。它的建立代碼示例以下:程序員

		// 建立一個線程數量爲3的線程池
		ExecutorService pool = (ExecutorService) Executors.newFixedThreadPool(3);

 

三、擁有無限數量線程的線程池,該線程池由Executors類的newCachedThreadPool方法建立而來。它的建立代碼示例以下:多線程

		// 建立一個不限制線程數量的線程池
		ExecutorService pool = (ExecutorService) Executors.newCachedThreadPool();

 

四、線程數量容許變化的線程池,該線程池須要調用ThreadPoolExecutor的構造方法來建立,構造方法的輸入參數按順序說明以下:
第一個參數是個整型數,名叫corePoolSize,它指定了線程池的最小線程個數。
第二個參數也是個整型數,名叫maximumPoolSize,它指定了線程池的最大線程個數。
第三個參數是個長整數,名叫keepAliveTime,它指定了每一個線程保持活躍的時長,若是某個線程的空閒時間超過這個時長,則該線程會結束運行,直到線程池中的線程總數等於corePoolSize爲止。
第四個參數爲TimeUnit類型,名叫unit,它指定了第三個參數的時間單位,好比TimeUnit.SECONDS表示時間單位是秒。
第五個參數爲BlockingQueue類型,它指定了待執行線程所處的等待隊列。
第四種線程池(自定義線程池)的建立代碼示例以下:併發

		// 建立一個自定義規格的線程池(最小線程個數爲2,最大線程個數爲5,每一個線程保持活躍的時長爲60,時長單位秒,等待隊列大小爲19)
		ThreadPoolExecutor pool = new ThreadPoolExecutor(
				2, 5, 60, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>(19));

 

建立好了線程池以後,便可調用線程池對象的execute方法將指定任務加入線程池。須要注意的是,execute方法並不必定馬上執行指定任務,只有當線程池中存在空閒線程或者容許建立新線程之時,纔會立刻執行任務;不然會將該任務放到等待隊列,而後按照排隊順序在方便的時候再一個一個執行隊列中的任務。除了execute方法方法,ExecutorService還提供了若干查詢與調度方法,這些方法的用途簡介以下:
getCorePoolSize:獲取核心的線程個數(即線程池的最小線程個數)。
getMaximumPoolSize:獲取最大的線程個數(即線程池的最大線程個數)。
getPoolSize:獲取線程池的當前大小(即線程池的當前線程個數)。
getTaskCount:獲取全部的任務個數。
getActiveCount:獲取活躍的線程個數。
getCompletedTaskCount:獲取已完成的任務個數。
remove:從等待隊列中移除指定任務。
shutdown:關閉線程池。關閉以後不能再往線程池中添加任務,不過要等已添加的任務執行完,才最終關掉線程池。
shutdownNow:當即關閉線程池。以後一樣不能再往線程池中添加任務,同時會給已添加的任務發送中斷信號,直到全部任務都退出才最終關掉線程池。
isShutdown:判斷線程池是否已經關閉。ide

接下來作個實驗,看看幾種線程池是否符合預期的運行方式。實驗開始前先定義一個操做任務,很簡單,僅僅打印本次的操做日誌,包括操做時間、操做線程、操做描述等信息。操做任務的代碼例子以下所示:工具

	// 定義一個操做任務
	private static class Operation implements Runnable {
		private String name; // 任務名稱
		private int index; // 任務序號
		public Operation(String name, int index) {
			this.name = name;
			this.index = index;
		}
		
		@Override
		public void run() {
			// 如下打印操做日誌,包括操做時間、操做線程、操做描述等信息
			String desc = String.format("%s執行到了第%d個任務", name, index+1);
			PrintUtils.print(Thread.currentThread().getName(), desc);
		}
	};

 

而後分別命令每種線程池各自啓動十個上述的操做任務。首先是單線程的線程池,它的實驗代碼示例以下:測試

	// 測試單線程的線程池
	private static void testSinglePool() {
		// 建立一個只有一個線程的線程池
		ExecutorService pool = (ExecutorService) Executors.newSingleThreadExecutor();
		for (int i=0; i<10; i++) { // 循環啓動10個任務
			// 建立一個操做任務
			Operation operation = new Operation("單線程的線程池", i);
			pool.execute(operation); // 命令線程池執行該任務
		}
		pool.shutdown(); // 關閉線程池
	}

 

運行以上的實驗代碼,觀察到以下的線程池日誌:this

22:22:43.959 pool-1-thread-1 單線程的線程池執行到了第1個任務
22:22:43.960 pool-1-thread-1 單線程的線程池執行到了第2個任務
22:22:43.961 pool-1-thread-1 單線程的線程池執行到了第3個任務
22:22:43.961 pool-1-thread-1 單線程的線程池執行到了第4個任務
22:22:43.962 pool-1-thread-1 單線程的線程池執行到了第5個任務
22:22:43.962 pool-1-thread-1 單線程的線程池執行到了第6個任務
22:22:43.962 pool-1-thread-1 單線程的線程池執行到了第7個任務
22:22:43.963 pool-1-thread-1 單線程的線程池執行到了第8個任務
22:22:43.963 pool-1-thread-1 單線程的線程池執行到了第9個任務
22:22:43.963 pool-1-thread-1 單線程的線程池執行到了第10個任務

 

由日誌可見,單線程的線程池始終只有一個名叫pool-1-thread-1的線程在執行任務。線程

繼續測試固定數量的線程池,它的實驗代碼示例以下:日誌

	// 測試固定數量的線程池
	private static void testFixedPool() {
		// 建立一個線程數量爲3的線程池
		ExecutorService pool = (ExecutorService) Executors.newFixedThreadPool(3);
		for (int i=0; i<10; i++) { // 循環啓動10個任務
			// 建立一個操做任務
			Operation operation = new Operation("固定數量的線程池", i);
			pool.execute(operation); // 命令線程池執行該任務
		}
		pool.shutdown(); // 關閉線程池
	}

 

運行以上的實驗代碼,觀察到以下的線程池日誌:

22:23:15.141 pool-1-thread-1 固定數量的線程池執行到了第1個任務
22:23:15.141 pool-1-thread-2 固定數量的線程池執行到了第2個任務
22:23:15.141 pool-1-thread-3 固定數量的線程池執行到了第3個任務
22:23:15.142 pool-1-thread-1 固定數量的線程池執行到了第4個任務
22:23:15.142 pool-1-thread-3 固定數量的線程池執行到了第5個任務
22:23:15.142 pool-1-thread-2 固定數量的線程池執行到了第6個任務
22:23:15.142 pool-1-thread-3 固定數量的線程池執行到了第7個任務
22:23:15.143 pool-1-thread-2 固定數量的線程池執行到了第8個任務
22:23:15.143 pool-1-thread-1 固定數量的線程池執行到了第9個任務
22:23:15.143 pool-1-thread-2 固定數量的線程池執行到了第10個任務

 

由日誌可見,固定數量的線程池一共開啓了三個線程去執行任務。

再來測試無限數量的線程池,它的實驗代碼示例以下:

	// 測試無限數量的線程池
	private static void testUnlimitPool() {
		// 建立一個不限制線程數量的線程池
		ExecutorService pool = (ExecutorService) Executors.newCachedThreadPool();
		for (int i=0; i<10; i++) { // 循環啓動10個任務
			// 建立一個操做任務
			Operation operation = new Operation("無限數量的線程池", i);
			pool.execute(operation); // 命令線程池執行該任務
		}
		pool.shutdown(); // 關閉線程池
	}

 

運行以上的實驗代碼,觀察到以下的線程池日誌:

22:25:52.344 pool-1-thread-6 無限數量的線程池執行到了第6個任務
22:25:52.344 pool-1-thread-3 無限數量的線程池執行到了第3個任務
22:25:52.344 pool-1-thread-5 無限數量的線程池執行到了第5個任務
22:25:52.344 pool-1-thread-8 無限數量的線程池執行到了第8個任務
22:25:52.344 pool-1-thread-7 無限數量的線程池執行到了第7個任務
22:25:52.344 pool-1-thread-4 無限數量的線程池執行到了第4個任務
22:25:52.344 pool-1-thread-1 無限數量的線程池執行到了第1個任務
22:25:52.344 pool-1-thread-9 無限數量的線程池執行到了第9個任務
22:25:52.344 pool-1-thread-2 無限數量的線程池執行到了第2個任務
22:25:52.344 pool-1-thread-10 無限數量的線程池執行到了第10個任務

 

由日誌可見,無限數量的線程池真的沒限制線程個數,有多少任務就啓動多少線程,雖然跑得很快可是系統壓力也大。

最後是自定義的線程池,它的實驗代碼示例以下:

	// 測試自定義的線程池
	private static void testCustomPool() {
		// 建立一個自定義規格的線程池(最小線程個數爲2,最大線程個數爲5,每一個線程保持活躍的時長爲60,時長單位秒,等待隊列大小爲19)
		ThreadPoolExecutor pool = new ThreadPoolExecutor(
				2, 5, 60, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>(19));
		for (int i=0; i<10; i++) { // 循環啓動10個任務
			// 建立一個操做任務
			Operation operation = new Operation("自定義的線程池", i);
			pool.execute(operation); // 命令線程池執行該任務
		}
		pool.shutdown(); // 關閉線程池
	}

 

運行以上的實驗代碼,觀察到以下的線程池日誌:

22:28:46.337 pool-1-thread-1 自定義的線程池執行到了第1個任務
22:28:46.337 pool-1-thread-2 自定義的線程池執行到了第2個任務
22:28:46.338 pool-1-thread-2 自定義的線程池執行到了第4個任務
22:28:46.338 pool-1-thread-1 自定義的線程池執行到了第3個任務
22:28:46.339 pool-1-thread-2 自定義的線程池執行到了第5個任務
22:28:46.339 pool-1-thread-1 自定義的線程池執行到了第6個任務
22:28:46.339 pool-1-thread-2 自定義的線程池執行到了第7個任務
22:28:46.339 pool-1-thread-1 自定義的線程池執行到了第8個任務
22:28:46.340 pool-1-thread-2 自定義的線程池執行到了第9個任務
22:28:46.340 pool-1-thread-1 自定義的線程池執行到了第10個任務

 

由日誌可見,自定義的線程池一般僅保持最小量的線程數,只有短期涌入大批任務的時候,纔會把線程數加碼到最大數量。



更多Java技術文章參見《Java開發筆記(序)章節目錄

相關文章
相關標籤/搜索