通過手擼線程池深入理解其原理(上)

==> 學習彙總(持續更新)
==> 從零搭建後端基礎設施系列(一)-- 背景介紹


摘要:源碼這東西看着能似懂非懂,有些地方你不知道人家爲什麼這麼設計,過後在想可能又忘了,很沒有效率。所以我推薦的學習順序是看書->看源碼->造輪子->總結。這一套下來,花的時間確實多,但是毫不誇張的說,能一勞永逸,那一個個知識點就像印在你腦子裏一樣。所以這次我從一個最簡單的線程池開始,帶着每一版遇到的問題,將線程池的各種核心功能逐一給造出來,最後再結合java線程池源碼一起分析。所以共分三篇講,上篇主要講不帶鎖的線程池如何實現,中篇主要講帶鎖的線程池如何實現,下篇主要分析自己實現的線程池和java線程池的異同點。

一、ThreadPoolV1
我們先忘掉之前使用過的線程池,來想象一下,最簡單的線程池只需要擁有什麼就行了?沒錯,有一堆線程就行了。所以,我們來看看V1版線程池,非常的簡單。
線程池參數:

  • workers:工作線程

代碼:

public class ThreadPoolV1 {
    //存放工作線程的哈希表
    private HashSet<Worker> workers;

    public ThreadPoolV1(){
        this.workers = new HashSet<>();
    }

    //執行任務
    public void submit(Runnable task){
        Worker w = new Worker(task);
        workers.add(w);
        w.thread.start();
    }

    //工作線程類
    private class Worker implements Runnable {
        Thread thread;
        Runnable task;

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

        @Override
        public void run() {
            task.run();
        }
    }
}

注:這裏的worker類沒有一點用,但是爲了跟下面的版本對齊,直接在V1版引出,不妨礙理解
測試代碼:

public static void main(String[] args) {
   ThreadPoolV1 pool = new ThreadPoolV1();
   for (int i = 0; i < 4; i++) {
       pool.submit(() -> {
           System.out.println("當前線程:" + Thread.currentThread().getName() + " 開始");
           try {
               Thread.sleep(1000);
           } catch (InterruptedException e) {
               e.printStackTrace();
           }
           System.out.println("當前線程:" + Thread.currentThread().getName() + " 結束");
       });
   }
}

測試結果:
在這裏插入圖片描述

問題分析:
這個V1版本的線程池問題非常多,我們一點點的去深挖,從最淺的開始分析。

  • 沒有控制線程池的大小 – 即缺少poolSize參數

  • 沒有循環利用線程 – 即線程創建後執行一次任務就銷燬了

這就相當於,這個池子漏水的,你放多少水進來,就流出去多少。這也是新手去理解線程池時糾結的一點,它是如何做到線程複用的呢?針對這兩個問題,進行改進得到V2版本。

二、ThreadPoolV2
那麼如何讓線程池容量有上限呢?很簡單,加一個線程池大小的參數限制一下即可。那如何複用線程呢?讓它可以不斷的執行新的任務?看過線程池源碼的都知道,需要一個任務隊列,線程池裏面的線程就不斷的從隊列中拿到新的任務去執行,思路有了,就開淦。
線程池參數:

  • workers:工作線程
  • poolSize:線程池大小
  • workerQueues:任務隊列

代碼:

public class ThreadPoolV2 {
    //線程數
    private int poolSize;

    //存放工作線程的哈希表
    private HashSet<Worker> workers;

    //任務隊列
    private BlockingDeque<Runnable> workerQueues;

    public ThreadPoolV2(int poolSize, BlockingDeque<Runnable> workerQueues){
        this.poolSize = poolSize;
        this.workers = new HashSet<>(poolSize);
        this.workerQueues = workerQueues;
    }

    //執行任務
    public void submit(Runnable task){
        if(workers.size() == poolSize){
            workerQueues.add(task);
        } else {
            Worker w = new Worker(task);
            workers.add(w);
            w.thread.start();
        }
    }

    //工作線程類
    private class Worker implements Runnable {
        Thread thread;
        Runnable firstTask;

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

        @Override
        public void run() {
            Runnable t = this.firstTask;
            this.firstTask = null;
            //剛開始第一個任務是不爲空的,執行完第一個任務後,繼續從隊列裏面獲取新的任務執行
            while (t != null ||  (t = getTask()) != null){
                t.run();
                //執行完要把任務置空,否則會重複執行
                t = null;
            }
        }
    }
    
    private Runnable getTask(){
        for (;;){
            try {
                //阻塞,一直到有任務爲止
                return workerQueues.take();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

測試代碼:

public static void main(String[] args) {
   ThreadPoolV2 pool = new ThreadPoolV2(2, new LinkedBlockingDeque<>());
   for (int i = 0; i < 4; i++) {
       pool.submit(() -> {
           System.out.println("當前線程:" + Thread.currentThread().getName() + " 開始");
           try {
               Thread.sleep(1000);
           } catch (InterruptedException e) {
               e.printStackTrace();
           }
           System.out.println("當前線程:" + Thread.currentThread().getName() + " 結束");
       });
   }
}

測試結果:
在這裏插入圖片描述

問題分析:
V2版本,能控制線程池大小,超出線程池大小的任務會進入到任務隊列中排隊等待空閒線程去獲取執行。這一切看起來似乎挺美好,但是問題還是很多,來分析一下,有哪些明顯的問題。

  • 沒有空閒線程自動回收功能,想象這樣一個場景,某一時刻任務突增,線程池被撐滿,但是很快任務量又下來了,並且持續很長時間都沒有任務量的突增,這會導致創建出來的很多線程空閒下來了,白白消耗了系統的資源。

  • 線程池沒有關閉功能,我們都知道,所有非常守護線程退出後,程序才能正常退出。

針對這兩個問題,進行改進得到V3版本。

三、ThreadPoolV3
那麼如何讓線程池自動回收空閒線程呢?很簡單,當在指定時間內獲取不到任務的時候,那麼就正常退出即可。那如何關閉線程池呢?這個也簡單,加一個布爾參數控制一下就行了,思路有了,就開淦。
線程池參數:

  • workers:工作線程
  • poolSize:線程池大小
  • workerQueues:任務隊列
  • RUNNING:線程池是否運行

代碼:

public class ThreadPoolV3 {
    //線程池大小
    private int poolSize;

    //存放工作線程的哈希表
    private HashSet<Worker> workers;

    //線程池是否關閉
    private boolean RUNNING = true;

    //任務隊列
    private BlockingDeque<Runnable> workerQueues;

    public ThreadPoolV3(int poolSize, BlockingDeque<Runnable> workerQueues){
        this.poolSize = poolSize;
        this.workers = new HashSet<>(poolSize);
        this.workerQueues = workerQueues;
    }

    //執行任務
    public void submit(Runnable task){
        if(RUNNING){
            // 當工作線程小於線程池大小時,創建新的線程處理
            if(workers.size() < poolSize){
                Worker w = new Worker(task);
                workers.add(w);
                w.thread.start();
            } else {
                //超過最大線程數時,就加入任務隊列
                workerQueues.add(task);
            }
        }
    }

    //關閉線程池
    public void shutdown(){
        RUNNING = false;
    }

    //工作線程類
    private class Worker implements Runnable {
        Thread thread;
        Runnable task;

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

        @Override
        public void run() {
            Runnable t = this.task;
            this.task = null;
            while (t != null || (t = getTask()) != null){
                t.run();
                t = null;
            }
            workers.remove(this);
            System.out.println("當前線程:" + Thread.currentThread().getName() + " 退出");
        }

        private Runnable getTask(){
            for (;;){
                //如果線程池關閉了,那麼沒必要自旋了
                if(!RUNNING && workerQueues.isEmpty()) return null;
                try {
                    //如果超時還未獲取到新的任務,會返回null,那麼當前線程就會退出銷燬了
                    return workerQueues.pollFirst(1000, TimeUnit.MILLISECONDS);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

測試代碼:

public static void main(String[] args) throws InterruptedException {
        ThreadPoolV3 pool = new ThreadPoolV3(2, new LinkedBlockingDeque<>());
        for (int i = 0; i < 4; i++) {
            pool.submit(() -> {
                System.out.println("當前線程:" + Thread.currentThread().getName() + " 開始");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("當前線程:" + Thread.currentThread().getName() + " 結束");
            });
        }
        Thread.sleep(5000);
        for (int i = 0; i < 4; i++) {
            pool.submit(() -> {
                System.out.println("當前線程:" + Thread.currentThread().getName() + " 開始");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("當前線程:" + Thread.currentThread().getName() + " 結束");
            });
        }
        Thread.sleep(5000);
        pool.shutdown();
    }

測試結果:
在這裏插入圖片描述

問題分析:
V3版本,在V2的基礎上,支持空閒線程自動回收。支持線程池關閉。但是從測試結果看,線程回收比較粗魯,一股腦的給你回收完了,導致下次再有任務的時候,又去創建了兩個線程,這種頻繁創建銷燬的操作,是非常損耗性能的,和線程池的思想不符合。所以得出以下問題

  • 線程池回收線程太粗魯,應該有更好的策略,比如分爲核心線程(常駐內存不銷燬)和非核心線程(指定空閒時間自動銷燬)

針對這個問題,進行改進得到V4版本。

四、ThreadPoolV4
那麼如何讓線程池自動回收空閒線程的時候將核心線程和非核心線程區分開呢?在這裏大家可能有一個誤區,認爲核心線程是固定不變的,不會進行銷燬,那就理解錯了,所謂的核心線程是指,只要在內存中常駐,不會被回收,僅此而已。簡單的說,就是你只要給我在線程池保留這麼多個線程就行,思路有了,就開淦。
線程池參數:

  • workers:工作線程
  • corePoolSize:核心線程數
  • maxPoolSize:最大線程數
  • keepTimeAlive:線程空閒存活的時間
  • TimeUnit:時間單位
  • workerQueues:任務隊列
  • RUNNING:線程池是否運行

代碼:

public class ThreadPoolV4 {
    //核心線程數
    private int corePoolSize;

    //最大線程數
    private int maxPoolSize;

    //允許線程的空閒時間
    private long keepTimeAlive;

    //存放工作線程的哈希表
    private HashSet<Worker> workers;

    //線程池是否關閉
    private boolean RUNNING = true;

    //任務隊列
    private BlockingDeque<Runnable> workerQueues;

    public ThreadPoolV4(int corePoolSize, int maxPoolSize, long keepTimeAlive, TimeUnit timeUnit, BlockingDeque<Runnable> workerQueues){
        this.corePoolSize = corePoolSize;
        this.maxPoolSize = maxPoolSize;
        this.keepTimeAlive = timeUnit.toNanos(keepTimeAlive);
        this.workers = new HashSet<>(corePoolSize);
        this.workerQueues = workerQueues;
    }

    //執行任務
    public void submit(Runnable task){
        if(RUNNING){
            /*
                1.當前線程數小於核心線程數時,創建新的工作線程處理
                2.當前線程數等於核心線程數時,加入任務隊列
                3.當任務隊列滿時,創建新的工作線程
                4.當工作線程達到最大線程數時,拒絕提交新的任務
             */
            if(workers.size() < corePoolSize){
                addWorker(task);
                //如果隊列滿了,會返回false
            } else if(workerQueues.offer(task)){

            } else if(workers.size() < maxPoolSize){
                addWorker(task);
            } else {
                throw new RuntimeException("線程池已滿,拒絕提交任務");
            }
        }
    }

    //關閉線程池
    public void shutdown(){
        RUNNING = false;
    }

    //創建新的工作線程
    private void addWorker(Runnable task){
        Worker w = new Worker(task);
        workers.add(w);
        w.thread.start();
    }

    //工作線程類
    private class Worker implements Runnable {
        Thread thread;
        Runnable task;

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

        @Override
        public void run() {
            Runnable t = this.task;
            this.task = null;
            while (t != null || (t = getTask()) != null){
                t.run();
                t = null;
            }
            workers.remove(this);
            System.out.println("當前線程:" + Thread.currentThread().getName() + " 退出");
        }

        private Runnable getTask(){
            for (;;){
                //如果線程池關閉 並且 工作隊列爲空,那麼可以回收該線程
                if(!RUNNING && workerQueues.isEmpty()) return null;
                try {
                    //如果超時未拿到任務 並且 當前線程數大於核心線程數的時候,就可以回收該線程
                    int wc = workers.size();
                    if(wc > corePoolSize) {
                        return workerQueues.poll(keepTimeAlive, TimeUnit.NANOSECONDS);
                    } 
                    return workerQueues.take();
                 } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

測試代碼:

public static void main(String[] args) throws InterruptedException {
    ThreadPoolV4 pool = new ThreadPoolV4(2, 4, 1, TimeUnit.SECONDS, new LinkedBlockingDeque<>(2));
    try {
        for (int i = 0; i < 6; i++) {
            pool.submit(() -> {
                System.out.println("當前線程:" + Thread.currentThread().getName() + " 開始");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("當前線程:" + Thread.currentThread().getName() + " 結束");
            });
        }
        Thread.sleep(5000);
        for (int i = 0; i < 2; i++) {
            pool.submit(() -> {
                System.out.println("當前線程:" + Thread.currentThread().getName() + " 開始");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("當前線程:" + Thread.currentThread().getName() + " 結束");
            });
        }
        Thread.sleep(5000);
    } finally {
        //關閉線程池
        pool.shutdown();
    }
}

測試結果:
在這裏插入圖片描述

問題分析:
V4版本,在V3的基礎上,支持核心線程常駐和非核心線程自動回收。從測試結果也能看出來,創建了2個核心和兩個非核心線程,最後還剩下兩個核心線程常駐內存。細心的同學可能已經發現個問題,那就是線程池已經關閉了,爲什麼程序還在運行?原因就在於getTask裏面的那段代碼,我們來分析一下:

private Runnable getTask(){
   for (;;){
       //如果線程池關閉 並且 工作隊列爲空,那麼可以回收該線程
       if(!RUNNING && workerQueues.isEmpty()) return null;
       try {
           //如果超時未拿到任務 並且 當前線程數大於核心線程數的時候,就可以回收該線程
           int wc = workers.size();
           if(wc > corePoolSize) {
               return workerQueues.poll(keepTimeAlive, TimeUnit.NANOSECONDS);
           }
           return workerQueues.take();
        } catch (InterruptedException e) {
           e.printStackTrace();
       }
   }
}

可以看到,當非核心線程回收完畢後,核心線程就會調用take()進行阻塞,導致自旋停止了。

五、總結
沒有鎖的線程池進行到V4版已經差不多了,接下來會根據V4版改進得到帶鎖的線程池V5版,那時候就會解決V4版的問題。最後,稍微總結一下無鎖線程池最終V4版的特點。

  • 支持線程池容量設置
  • 支持線程複用,不斷執行新任務
  • 支持空閒指定時間的線程自動回收
  • 支持任務排隊
  • 支持線程池關閉,清理線程

以上線程池都只支持單線程提交任務,反之則會出現不可預知的結果。

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