線程池系列 (1) 讓多線程不再坑爹的線程池

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排隊等待獲得線程資源的機會。

關於併發編程中的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

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