【併發基礎】線程池實現原理分析

什麼是線程池

  • 線程池就是系統爲了方便管理線程而事先創建一些緩衝線程,它們的集合稱爲線程池

使用線程池的好處

  • 第一:降低資源消耗。通過重複利用已創建的線程降低線程創建和銷燬造成的消耗。
  • 第二:提高響應速度。當任務到達時,任務可以不需要等到線程創建就能立即執行。
  • 第三:提高線程的可管理性。線程是稀缺資源,如果無限制地創建,不僅會消耗系統資源

ThreadPoolExecutor

Executors

Executors工具類提供了幾個靜態方法供我們創建線程池,但是都不被阿里規約推薦使用,下面分析其原因

  • newCachedThreadPool
    創建一個可緩存線程池,如果線程池長度超過處理需要,可靈活回收空閒線程,若無可回收,則新建線程。
    線程池參數中最大線程數爲最大整數值,可能發生OOM
  • newFixedThreadPool
    創建一個固定大小線程池, 其中阻塞隊列爲最大整數值,可能發生OOM
  • newScheduledThreadPool
    創建一個支持週期性調度的線程池, 其中最大線程數爲最大整數值,可能發生OOM
  • newSingleThreadExecutor
    創建一個單線程化的線程池, 其中阻塞隊列爲最大整數值,可能發生OOM

核心參數解釋

  • corePoolSize核心線程數即核心池的大小。 當有任務來之後,就會創建一個線程去執行任務,當線程池中的線程數目達到corePoolSize後,就會把到達的任務放到緩存隊列當中
  • maximumPoolSize: 線程池最大線程數,它表示在線程池中最多能創建多少個線程;超過最大線程數時將採用拒絕策略
  • keepAliveTime: 表示線程沒有任務執行時最多保持多久時間會終止。
    unit: 參數keepAliveTime的時間單位,有7種取值,在TimeUnit類中有7種靜態屬性:

工作流程

提交一個任務到線程池中,線程池的處理流程如下:

1、判斷線程池裏的核心線程是否都在執行任務,如果不是(核心線程空閒或者還有核心線程沒有被創建)則創建一個新的工作線程來執行任務。如果核心線程都在執行任務,則進入下個流程2。
2、線程池判斷工作隊列是否已滿,如果工作隊列沒有滿,則將新提交的任務存儲在這個工作隊列裏。如果工作隊列滿了,則進入下個流程3。
3、判斷線程池裏的線程是否大於最大線程數,如果不大於,則創建線程進入等待隊列。如果已經阻塞隊列也滿了,則交給飽和策略來處理這個任務。
原理圖

自定義線程線程池

注意事項

  • 如果當前線程池中的線程數目**<corePoolSize**,則每來一個任務,就會創建一個線程去執行這個任務;
  • 如果當前線程池中的線程數目**>=corePoolSize**,則每來一個任務,會嘗試將其添加到任務緩存隊列當中,若添加成功,則該任務會等待空閒線程將其取出去執行;若添加失敗(一般來說是任務緩存隊列已滿),則會嘗試創建新的線程去執行這個任務;
  • 如果緩存隊列已經滿了,則在總線程數不大於maximumPoolSize的前提下,則創建新的線程
  • 如果當前線程池中的線程數目達到maximumPoolSize,則會採取任務拒絕策略進行處理;
  • 如果線程池中的線程數量大於 corePoolSize時,如果某線程空閒時間超過keepAliveTime,線程將被終止,直至線程池中的線程數目不大於corePoolSize;
  • 如果允許爲核心池中的線程設置存活時間,那麼核心池中的線程空閒時間超過keepAliveTime,線程也會被終止。

使用ThreadPoolExecutor創建線程池例子

public class Test0007 {

	public static void main(String[] args) {
		ThreadPoolExecutor executor = new ThreadPoolExecutor(1, 2, 60L, TimeUnit.SECONDS, new ArrayBlockingQueue<>(3));
		for (int i = 1; i <= 6; i++) {
			TaskThred t1 = new TaskThred("任務" + i);
			executor.execute(t1);
		}
		executor.shutdown();
	}
}

class TaskThred implements Runnable {
	private String taskName;

	public TaskThred(String taskName) {
		this.taskName = taskName;
	}

	@Override
	public void run() {
		System.out.println(Thread.currentThread().getName()+taskName);
	}

}

如何合理配置線程池

CPU密集型

CPU密集的意思是該任務需要大量的運算,而沒有阻塞,CPU一直全速運行。
CPU密集任務只有在真正的多核CPU上纔可能得到加速(通過多線程),而在單核CPU上,無論你開幾個模擬的多線程,該任務都不可能得到加速,因爲CPU總的運算能力就那些。

IO密集

IO密集型,即該任務需要大量的IO,即大量的阻塞。在單線程上運行IO密集型的任務會導致浪費大量的CPU運算能力浪費在等待。所以在IO密集型任務中使用多線程可以大大的加速程序運行,即時在單核CPU上,這種加速主要就是利用了被浪費掉的阻塞時間。

分析角度

  1. 任務的性質:CPU密集型任務、IO密集型任務、混合型任務。
  2. 任務的優先級:高、中、低。
  3. 任務的執行時間:長、中、短。
  4. 任務的依賴性:是否依賴其他系統資源,如數據庫連接等。

最佳實踐

  • CPU密集型時,任務可以少配置線程數,大概和機器的cpu核數相當,這樣可以使得每個線程都在執行任務
  • IO密集型時,大部分線程都阻塞,故需要多配置線程數,2*cpu核數

一個基於線程池技術的簡單Web服務器

	public class SimpleHttpServer {
		// 處理HttpRequest的線程池 
		static ThreadPool < HttpRequestHandler > threadPool = new DefaultThreadPool < HttpRequestHandler > (1);
		// SimpleHttpServer的根路徑
		static String basePath;
		static ServerSocket serverSocket;
		// 服務監聽端口 
		static int port = 8080;
		public static void setPort(int port) {
			if (port > 0) {
				SimpleHttpServer.port = port;
			}
		}
		public static void setBasePath(String basePath) {
			if (basePath != null && new File(basePath).exists() && new File(basePath).isDirectory()) {
				SimpleHttpServer.basePath = basePath;
			}
		}
		// 啓動SimpleHttpServer public static void start() throws Exception { serverSocket = new ServerSocket(port);
		Socket socket = null;
		while ((socket = serverSocket.accept()) != null) {
			// 接收一個客戶端Socket,生成一個HttpRequestHandler,放入線程池執行 
			threadPool.execute(new HttpRequestHandler(socket));
		}
		serverSocket.close();
	}
	static class HttpRequestHandler implements Runnable {
		private Socket socket;
		public HttpRequestHandler(Socket socket) {
			this.socket = socket;
		}
		@Override public void run() {
			String line = null;
			BufferedReader br = null;
			BufferedReader reader = null;
			PrintWriter out = null;
			InputStream in = null;
			try {
				reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
				String header = reader.readLine();
				// 由相對路徑計算出絕對路徑 
				String filePath = basePath + header.split(" ")[1];
				out = new PrintWriter(socket.getOutputStream());
				// 如果請求資源的後綴爲jpg或者ico,則讀取資源並輸出 
				if (filePath.endsWith("jpg") || filePath.endsWith("ico")) {
					in = new FileInputStream(filePath);
					ByteArrayOutputStream baos = new ByteArrayOutputStream();
					int i = 0;
					while ((i = in.read()) != -1) {
						baos.write(i);
					}
					byte[] array = baos.toByteArray();
					out.println("HTTP/1.1 200 OK");
					out.println("Server: Molly");
					out.println("Content-Type: image/jpeg");
					out.println("Content-Length: " + array.length);
					out.println("");
					socket.getOutputStream().write(array, 0, array.length);
				} else {
					br = new BufferedReader(new InputStreamReader(new FileInputStream(filePath)));
					out = new PrintWriter(socket.getOutputStream());
					out.println("HTTP/1.1 200 OK");
					out.println("Server: Molly");
					out.println("Content-Type: text/html; charset=UTF-8");
					out.println("");
					while ((line = br.readLine()) != null) {
						out.println(line);
					}
				}
				out.flush();
			}
			catch (Exception ex) {
				out.println("HTTP/1.1 500");
				out.println("");
				out.flush();
			}
			finally {
				close(br, in , reader, out, socket);
			}
		}
	}
	// 關閉流或者Socket 
	private static void close(Closeable... closeables) {
		if (closeables != null) {
			for (Closeable closeable : closeables) {
				try {
					closeable.close();
				}
				catch (Exception ex) {
				}
			}
		}
	}
}

對以上代碼進行QPS測試發現隨着線程池中線程數量的增加,SimpleHttpServer的吞吐量不斷增大,響應時間 不斷變小,線程池的作用非常明顯。 但是,線程池中線程數量並不是越多越好,具體的數量需要評估每個任務的處理時間,以 及當前計算機的處理器能力和數量。使用的線程過少,無法發揮處理器的性能;使用的線程過 多,將會增加系統的無故開銷,起到相反的作用。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章