徹底搞定線程池-(1)線程池模型的構建

概述

做後端的應該都知道線程池,即使你沒親自使用過,那也一定聽過或者瞭解過。有時候也會去深入理解,結果往往是當時覺得自己理解了,過一段時間就忘了。因爲在日常的開發中,我們都不需要用到線程池,很多都是使用的工具和框架寫好,我們直接調接口就完事了。

很多東西沒有親自實踐和深入的思考過的,單單看文章和書籍是不可能真正的理解的。以前我看了好多次線程池相關的文章,然後過個半年忘得差不多,又重新看,結果還是沒有真正的理解。所以,我就打算動手實踐一番,既然平時開發的時候用不到,那我就自己做一個項目來用上。

說做就做,我就選了一個Redis分佈式鎖作爲練手項目,裏面有一個定時續期的功能,就是使用線程池定時的運行提交的任務,將key續期,詳細的不說了,想了解可以看這個 Redis分佈式鎖的實現-Redisson

在實現Redis續期功能的時候,一邊看別人定時任務怎麼實現的,一邊看線程池的源碼。這時候我彷彿打開了新世界的大門,徹底理解了線程池運行邏輯,也瞭解了一些線程池設計的藝術。

接下來我想以一個設計者的角度,帶領大家從零去設計和實現一個線程池,一步一個腳印,徹底的理解線程池的實現以及一些設計的藝術。

線程池出現的目的和意義

我們要明白,任何技術都是爲了解決問題而出現的。那麼線程池的出現解決了什麼問題呢,答案是:解決了線程資源複用的問題

如果沒有線程池,我們處理一個任務就要新開一個線程去執行,當任務完成時,該線程就停止了。如果說這個任務是重複的,總不能來一個就新建一個線程吧,多浪費,而且線程是稀缺資源,重複的創建銷燬很耗時間。

有了線程池,我們就可以建立幾個固定線程。有任務來了喚醒閒置的線程去處理,任務處理完成後繼續處理後續的任務,如果暫時沒有任務,可以將線程休眠,有新任務時再喚醒。這樣一來就可以更高效的利用線程資源,提高系統併發效率。

任務:抽象的工作單元

在程序中,都是圍繞着任務執行來構造的,任務通常是一些抽象的工作單元。比如可以把一個 http請求 當做是一個任務,把一次與數據庫的交互當做任務等等。在線程池中,我們把要處理的東西抽象成一個任務單元, 這樣可以簡化線程池的結構,以此更好的構建線程池的模型。

線程:抽象的工作者

在線程池中,我們可以把每一個線程當做是一個worker,即"工人"的意思。它會不斷的嘗試獲得任務來執行,如果沒有任務,則休眠或者做其他處理。

線程池的功能設計

那麼,線程池通常要具備和提供什麼功能呢,這裏把核心的功能需求給羅列一下:

線程池的開啓和關閉

線程池作爲一個工具,需要有自己的生命週期,可以抽象成三個:

  • 開啓狀態
  • 運行狀態
  • 結束狀態

其中結束狀態下線程池的處理和考慮的東西要多一些,執行完線程池的關閉接口後:

  • 正在運行的任務怎麼處理?
  • 在任務隊列的任務要怎麼處理?
  • 此時線程池是否還能繼續添加任務?

這些東西都是要考慮的並且去處理的。在Java的ExecutorService 提供了兩個關閉接口

  • shutdown : 有序的關閉,已提交的任務會被逐一處理,但不會接受任何新任務
  • shutdownNow : 嘗試停止所有正在執行的任務,放棄在隊列中等待的任務,並返回正在等待執行的任務列表

線程的構建和管理

線程池裏線程該怎麼構建,構建完後怎麼管理,是固定的幾個還是動態的構建。這裏給出幾個模式:

  1. 固定的線程數量 :在線程池啓動時就構建固定數量的線程池,且不會關閉任何線程
  2. 動態構建線程 :啓動時不新建任何線程,當有任務來臨時纔會去創建線程。如果任務比較少,則不會繼續新建線程,如果任務比較多,則繼續構建線程數,直到數量達到最大值。
  3. 有閒置期限的線程 :線程在構建時會有一個閒置的期限,當閒置的時間超過期限時,該線程就會進行回收處理。這個在數據庫連接池比較常用到
  4. 單個線程 :只有一個線程,任務按提交的時間順序執行。

任務管理

在線程池中,會建立一個任務隊列,當沒有空閒線程時,新來的任務會放到隊列中,等待線程執行。

線程池要提供任務執行的接口。

另外,很多任務都會將處理結果作爲返回值的,這時任務要有一個完成後的處理機制,在任務完成時做某些操作。(這裏就要涉及到FutureTask相關概念了)

任務相關的功能如下:

  • 任務的提交
  • 任務處理結果
  • 任務的取消和中斷

線程池模型的構建

梳理了線程池的一些基本功能和要考慮的點,那麼線程池的執行過程是怎樣,要怎麼設計呢。廢話不說,直接上圖:
在這裏插入圖片描述

當有新任務時查看是否有空閒線程,如果有,直接處理,如果沒有則放到任務隊列中,等待線程處理。

其實梳理一下線程池,可以發現它的邏輯並不複雜,複雜的是各種情況的處理,比如線程怎麼管理,任務取消怎麼處理,線程中斷如何處理等等,還有各種併發操作的處理。

使用代碼實現簡易的線程池

接下來實現一個固定數量的線程池,當有任務提交時

線程池要提供的接口

  • 任務的提交

線程池內部要實現的功能

  • 任務隊列的實現
  • 線程管理

咱們暫時將線程池的核心功能簡單的實現,瞭解線程池的執行邏輯,其他的之後慢慢添加。

創建任務單元

首先將任務單元給實現了,直接實現Runnable 接口即可。

當然,可以不實現 Runnable 接口,隨便寫一個類,給一個執行接口,但是呢這樣線程池就不夠通用了,還是直接實現Runnable接口,往後任意實現該接口的任務都可以交給線程池執行。

static class Task implements Runnable{
    
        private int tag;

        public Task(int tag){
            this.tag = tag;
        }

        @Override
        public void run() {
            System.out.printf("任務 %d 開始執行 \n",tag);
            System.out.printf("任務 %d 執行中 \n",tag);
            System.out.printf("任務 %d 執行結束\n",tag);
        }
}

線程池的實現

詳細的說明在註釋中,看註釋就可以了

package steap1;

import java.util.concurrent.BlockingQueue;
import java.util.concurrent.locks.ReentrantLock;

public class ThreadPoolExecutor {

    //工作線程數組
    private Worker[] workers;

    //任務阻塞隊列,是線程安全的,裏面每個操作都會加鎖處理
    private BlockingQueue<Task> queue;

    // 當前工作線程的數量
    private int workerSize = 0;

    //線程池最大的工作線程數量
    private int poolSize;

    public ThreadPoolExecutor(int poolSize, BlockingQueue<Task> queue) {
        this.poolSize = poolSize;
        this.workers = new Worker[poolSize];
        this.queue = queue;
    }

    public void execute(Task task) {
        //如果線程池的線程數量小於最大值,則添加線程
        //否則將任務放入隊列中
        if (workerSize < poolSize) {
            addWorker(task);
        } else {
            this.queue.add(task);
        }
    }

    //添加worker工作線程,並立即執行
    private synchronized void addWorker(Task task) {
        //這裏做個雙重判定,判定線程數量是否小於最大值
        if (workerSize >= poolSize) {
            this.queue.add(task);
            return;
        }

        //構建worker,並啓動線程
        workers[workerSize] = new Worker(task);
        workers[workerSize].t.start();

        workerSize++;
    }

    //實際運行的代碼
    void runWorker(Worker worker){
        Task task =(Task) worker.task;
        try {
            while (true){
                //線程在這個循環中不斷的獲取任務來執行
                // queue.task() 方法是一個線程安全的阻塞方法
                //如果隊列沒有任務,那麼所有工作線程都會在這裏阻塞,等待獲取可用的任務
                if(task == null){
                    task = this.queue.take();
                }
                task.run();
                task = null;
            }
        }catch (InterruptedException e){
            e.printStackTrace();
        }
    }
    
    //工作線程包裝類
    private class Worker implements Runnable {
        private Runnable task;

        final Thread t;

        public Worker(Runnable task) {
            this.task = task;
            this.t = new Thread(this);
        }

        @Override
        public void run() {
            runWorker(this);
        }
    }

    //任務類
    static class Task implements Runnable {

        private int tag;

        public Task(int tag) {
            this.tag = tag;
        }

        @Override
        public void run() {
            System.out.printf("任務 %d 開始執行 \n", tag);
            System.out.printf("任務 %d 執行中 \n", tag);
            System.out.printf("任務 %d 執行結束\n", tag);
        }
    }
}

簡單的使用

    public static void main(String[] args){
        ThreadPoolExecutor executor = new ThreadPoolExecutor(8,new LinkedBlockingQueue<>());
        for(int i=0;i<1000;i++){
            executor.execute(new ThreadPoolExecutor.Task(i));
        }
    }

執行結果

任務 923 開始執行 
任務 923 執行中 
任務 923 執行結束
任務 912 開始執行 
任務 912 執行中 
任務 912 執行結束

總結

至此,一個簡單的線程池就編寫完畢,線程池主要的功能都實現了,整個執行過程也進行了詳細的描述。

其實這裏還有很多東西沒寫上,線程的生命週期管理,任務的取消和線程的中斷等等,這些東西在下一篇章完善吧。

結尾附上項目的源代碼,本章的內容在step1中。

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