Java併發 之 執行緒池系列 (1) 讓多執行緒不再坑爹的執行緒池

西召發表於2019-03-31

lady_pool_shit


背景

執行緒池的來由

服務端的程式,例如資料庫伺服器和Web伺服器,每次收到客戶端的請求,都會建立一個執行緒來處理這些請求。

建立執行緒的方式又很多,例如繼承Thread類、實現Runnable或者Callable介面等。

通過建立新的執行緒來處理客戶端的請求,這種看起來很容易的方法,其實是有很大弊端且有很高的風險的。

俗話說,簡單的路越走越困難,困難的路越走越簡單,就是這個道理。

建立和銷燬執行緒,會消耗大量的伺服器資源,甚至建立和銷燬執行緒消耗的時間比執行緒本身處理任務的時間還要長。

由於啟動執行緒需要消耗大量的伺服器資源,如果建立過多的執行緒會造成系統記憶體不足(run out of memory),因此限制執行緒建立的數量十分必要。

dogs_multithread_programming

什麼是執行緒池

執行緒池通俗來講就是一個取出和放回提前建立好的執行緒的池子,概念上,類似資料庫的連線池。

那麼執行緒池是如何發揮作用的呢?

實際上,執行緒池是通過重用之前建立好執行緒來處理當前任務,來達到大大降低執行緒頻繁建立和銷燬導致的資源消耗的目的。

A thread pool reuses previously created threads to execute current tasks and offers a solution to the problem of thread cycle overhead and resource thrashing. Since the thread is already existing when the request arrives, the delay introduced by thread creation is eliminated, making the application more responsive.

Thread Pool

背景總結

下面總結一下開篇對於執行緒池的一些介紹。

  1. 執行緒是程式的組成部分,可以幫助我們搞事情。
  2. 多個執行緒同時幫我們搞事情,可以通過更大限度地利用伺服器資源,用來大大提高我們搞事情的效率。
  3. 我們建立的每個執行緒都不是省油的燈,執行緒越多就會佔用越多的系統資源,因此小弟雖好使但不要貪多哦,在有限的系統資源下,執行緒並不是“韓信點兵,多多益善”的,要限制執行緒的數量。請記住這一條,因為下面“批判”Java提供的執行緒池建立解決方案的時候,這就是“罪魁禍首”。
  4. 建立和銷燬執行緒會耗費大量系統資源,就像大佬招募和遣散小弟,都是要大費周章的。因此聰明的大佬就想到了“池”,把執行緒快取起來,用的時候拿出來不用的時候還放回去,這就可以既享受多執行緒的樂趣,又可以避免使用多執行緒的痛苦了。

但到底怎麼使用執行緒池呢?執行緒池真的這麼簡單好用嗎?執行緒池使用的過程中有沒有什麼坑?

不要著急,下面就結合具體的示例,跟你講解各種使用執行緒池的姿勢,以及這些姿勢爽在哪裡,痛在哪裡。

準備好紙巾,咳咳...,是筆記本,濤哥要跟你開講啦!

用法

通過Executors建立執行緒池

Executors及其服務的類

java.util.concurrent.Executors是JDK的併發包下提供的一個工廠類(Factory)和工具類(Utility)。

Executors提供了關於Executor, ExecutorService, ScheduledExecutorService, ThreadFactoryCallable相關的工廠方法和工具方法。

Executor是一個執行提交的Runnable Tasks的物件,它有一個execute方法,引數是Runnable。當執行execute方法以後,會在未來某個時間,通過建立執行緒或者使用執行緒池中的執行緒的方式執行引數中的任務。用法如下:

Executor executor = anExecutor;
executor.execute(new RunnableTask1());
executor.execute(new RunnableTask2());
複製程式碼

ExecutorService繼承了Executor,並提供了更多有意思的方法,比如shutdown方法會讓ExecutorService拒絕建立新的執行緒來執行task。

Executors常用的幾個方法


//建立固定執行緒數量的執行緒池
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(10);

//建立一個執行緒池,該執行緒池會根據需要建立新的執行緒,但如果之前建立的執行緒可以使用,會重用之前建立的執行緒
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();

//建立一個只有一個執行緒的執行緒池
ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();

複製程式碼

一個執行緒池的例子

下面我就建立5個Task,並通過一個包含3個執行緒的執行緒池來執行任務。我們一起看下會發生什麼。

Github 完整程式碼: 一個執行緒池的例子

ThreadPoolExample1就是我們的測試類,下面所有的內部類、常量和方法都寫在這個測試類裡。

package net.ijiangtao.tech.concurrent.jsd.threadpool;

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ThreadPoolExample1 {

}
複製程式碼

任務

Task內部類執行了兩次for迴圈,並在每次迴圈執行結束以後 sleep 1秒鐘。

// Task class to be executed (Step 1)
static class Task implements Runnable {

	private String name;

	public Task(String s) {
		name = s;
	}

	// Prints task name and sleeps for 1s
	// This Whole process is repeated 2 times
	public void run() {
		try {
			for (int i = 0; i <= 1; i++) {
				if (i == 0) {
					//prints the initialization time for every task
					printTimeMsg("Initialization");
				} else {
					// prints the execution time for every task
					printTimeMsg("Executing");
				}
				Thread.sleep(1000);
			}
			System.out.println(name + " complete");
		} catch (InterruptedException e) {
				e.printStackTrace();
		}
	}

	private void printTimeMsg(String state) {
		Date d = new Date();
		SimpleDateFormat ft = new SimpleDateFormat("hh:mm:ss");
		System.out.println(state+" Time for"+ " task name - " + name + " = " + ft.format(d));
	}
}
複製程式碼

池子

建立一個固定執行緒數的執行緒池。

// Maximum number of threads in thread pool
static final int MAX_T = 3;
// creates a thread pool with MAX_T no. of
// threads as the fixed pool size(Step 2)
private static final ExecutorService pool = Executors.newFixedThreadPool(MAX_T);
複製程式碼

測試

建立5個任務,並通過執行緒池的執行緒執行這些任務。

public static void main(String[] args) {
	 // creates five tasks
	 Runnable r1 = new Task("task 1");
	 Runnable r2 = new Task("task 2");
	 Runnable r3 = new Task("task 3");
	 Runnable r4 = new Task("task 4");
	 Runnable r5 = new Task("task 5");

	 // passes the Task objects to the pool to execute (Step 3)
	 pool.execute(r1);
	 pool.execute(r2);
	 pool.execute(r3);
	 pool.execute(r4);
	 pool.execute(r5);

	 // pool shutdown ( Step 4)
	 pool.shutdown();
}
複製程式碼

執行結果如下。

Initialization Time for task name - task 1 = 12:39:44
Initialization Time for task name - task 2 = 12:39:44
Initialization Time for task name - task 3 = 12:39:44
Executing Time for task name - task 3 = 12:39:45
Executing Time for task name - task 1 = 12:39:45
Executing Time for task name - task 2 = 12:39:45
task 2 complete
Initialization Time for task name - task 4 = 12:39:46
task 3 complete
Initialization Time for task name - task 5 = 12:39:46
task 1 complete
Executing Time for task name - task 5 = 12:39:47
Executing Time for task name - task 4 = 12:39:47
task 5 complete
task 4 complete
複製程式碼

說明

從輸出的結果我們可以看到,5個任務在包含3個執行緒的執行緒池執行。

  1. 首先會有3個任務(task 1,task 2,task 3)獲得執行緒資源併發執行;
  2. (task 2)執行成功以後,讓出執行緒資源,(task 4)開始執行;
  3. (task 3)執行成功以後,讓出執行緒資源,(task 5)開始執行;
  4. 最終,5個任務都執行結束,執行緒池將執行緒資源回收。

由於執行緒的執行有一定的隨機性,以及不同機器的資源情況不同,每次的執行結果,可能會有差異。

下面是我第二次執行的結果。

Initialization Time for task name - task 1 = 12:46:33
Initialization Time for task name - task 3 = 12:46:33
Initialization Time for task name - task 2 = 12:46:33
Executing Time for task name - task 2 = 12:46:34
Executing Time for task name - task 3 = 12:46:34
Executing Time for task name - task 1 = 12:46:34
task 3 complete
task 2 complete
task 1 complete
Initialization Time for task name - task 4 = 12:46:35
Initialization Time for task name - task 5 = 12:46:35
Executing Time for task name - task 4 = 12:46:36
Executing Time for task name - task 5 = 12:46:36
task 5 complete
task 4 complete
複製程式碼

task 1 2 3 獲得執行緒資源,task 4 5排隊等待:

task 1 2 3 獲得執行緒資源,task 4 5排隊等待

task 1 2 3 執行結束,task 4 5獲得執行緒資源,執行緒池中有一個執行緒處於空閒狀態:

task 1 2 3 執行結束,task 4 5獲得執行緒資源

但規律是相同的,那就是執行緒池會將自己的執行緒資源貢獻出來,如果任務數超出了執行緒池的執行緒數,就會阻塞並排隊等待有可用的執行緒資源以後執行。

也就是執行緒池會保證你的task在將來(Future)的某個時間執行,但並不能保證什麼時間會執行。

相信你現在對於ExecutorServiceinvokeAll方法,可以執行一批task並返回一個Future集合,就會有更深入的理解了。

List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks) throws InterruptedException
複製程式碼

通過ExecutorService執行緒池執行task的過程如下圖所示,超出執行緒池執行緒數量的task將會在BlockingQueue排隊等待獲得執行緒資源的機會。

Java併發 之 執行緒池系列 (1) 讓多執行緒不再坑爹的執行緒池

關於併發程式設計中的Futrue,筆者有一篇文章(Java併發程式設計-Future系列之Future的介紹和基本用法)專門介紹,請通過下面任意的連結移步欣賞:

總結

本教程帶領大家瞭解了執行緒池的來由、概念和基本用法,相信大家看完,以後就不再只會傻傻地new Thread了。

本節只是執行緒池的入門,下面會介紹關於執行緒池的更多武功祕籍,希望大家持續關注,有所獲益。


Links

文章友鏈

相關資源

Concurrent-ThreadPool-執行緒池拒絕策略RejectedExecutionHandler

Concurrent-ThreadPool-ThreadPoolExecutor裡面4種拒絕策略

Concurrent-ThreadPool-執行緒池ThreadPoolExecutor構造方法和規則

Concurrent-ThreadPool-執行緒池的成長之路

Concurrent-ThreadPool-LinkedBlockingQueue和ArrayBlockingQueue的異同

Concurrent-ThreadPool-最佳執行緒數總結

Concurrent-ThreadPool-最佳執行緒數

Concurrent-ThreadPool-Thread Pools in Java

Concurrent-ThreadPool-java-thread-pool

Concurrent-ThreadPool-thread-pool-java-and-guava

Concurrent-ThreadPool-ijiangtao.net

相關文章