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

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


摘要:上篇實現了簡單的無鎖線程池,本篇開始實現有鎖線程池。先來思考一下,爲什麼線程池需要鎖?在沒有鎖的線程池中,就算是單線程提交,也可能會涉及到併發的問題,如果是多線程提交任務,這時候出錯的概率基本是百分百了。

一、什麼是鎖?
在開始寫代碼之前,先簡單認識一下鎖的本質是什麼,先來看一張圖。
在這裏插入圖片描述
中間那個管道就相當於鎖,不管外面有多少個線程,這個管道只能一一通過。

二、ThreadPoolV5
線程池參數:

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

代碼:

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

    //最大線程數
    private int maxPoolSize;

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

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

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

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

    //全局鎖
    private ReentrantLock lock = new ReentrantLock();

    //記錄線程池大小
    private int currentPoolSize = 0;

    public ThreadPoolV6(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;
    }
    static int c = 0;
    //執行任務
    public void submit(Runnable task){
        if(RUNNING){
            /*
                1.當前線程數小於核心線程數時,創建新的工作線程處理
                2.當前線程數等於核心線程數時,加入任務隊列
                3.當任務隊列滿時,創建新的工作線程
                4.當工作線程達到最大線程數時,拒絕提交新的任務
             */
            try {
                lock.lock();
                if(currentPoolSize < corePoolSize){
                    System.out.println("核心線程數:" + (++c));
                    addWorker(task);
                    //如果隊列滿了,會返回false
                } else if(workerQueues.offer(task)){

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

    //關閉線程池
    public void shutdown(){
        try {
            lock.lock();
            RUNNING = false;
            System.out.println("關閉前線程池大小:" + currentPoolSize);
            for (Worker worker : workers) {
                worker.thread.interrupt();
            }
        } finally {
            lock.unlock();
        }
    }

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

    //工作線程類
    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;
            }
            try {
                lock.lock();
                workers.remove(this);
            } finally {
                lock.unlock();
            }
            System.out.println("當前線程:" + Thread.currentThread().getName() + " 退出");
        }

        private Runnable getTask(){
            boolean timeout = false;
            for (;;){
                //如果線程池關閉 並且 工作隊列爲空,那麼可以回收該線程
                if(!RUNNING && workerQueues.isEmpty()) return null;

                try {
                    //如果超時未拿到任務 並且 當前線程數大於核心線程數的時候,就可以回收該線程
                    boolean timed;
                    try {
                        lock.lock();
                        timed = workers.size() > corePoolSize;
                        if(timed && timeout) {
                            --currentPoolSize;
                            return null;
                        }
                    } finally {
                        lock.unlock();
                    }
                    Runnable runnable = timed ? workerQueues.poll(keepTimeAlive, TimeUnit.NANOSECONDS) : workerQueues.take();
                    if(runnable != null){
                        return runnable;
                    }
                    timeout = true;
                } catch (InterruptedException e) {
                    timeout = false;
                }
            }
        }
    }
}

測試代碼:

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

測試結果:
在這裏插入圖片描述
問題分析:
看測試結果,加鎖後是符合預期的,非核心線程被回收,核心線程常駐內存。接下來分析還有哪些問題

  • submit裏的加鎖方式,有些粗魯,相當於把整個方法都給鎖了,效率會大打折扣。這樣會造成任務提交的時候其實是串行提交的,效率上並無任何提高,還因爲加鎖解鎖而損耗了性能。
  • 再來看getTask裏的加鎖方式,思想是非常好的,只鎖住了一小段執行非常快的關鍵代碼,那就是判斷當前線程池大小是否大於核心線程數,如果大於就可以回收,否則只能調用take()方法阻塞直到有新任務來臨。
  • 再來看shutdown()方法,和無鎖線程池的區別是,多了線程中斷,這能解決什麼問題呢?首先,在getTask()方法裏,take()會阻塞,如果你僅僅只是修改RUNNING的狀態,是不能夠關閉線程的。所以需要增加一個線程中斷,讓take()從阻塞中拋異常,然後我們捕獲處理即可。又或者是線程正在執行任務,這時候可以根據中斷狀態,自行決定是立刻中斷還是執行完任務再中斷。
  • 最後可以看看submit的返回值是void,滿足不了需要監控返回值的場景。

根據這四個問題進行改進得到V6版

三、ThreadPoolV6
線程池參數:

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

代碼:

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

    //最大線程數
    private int maxPoolSize;

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

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

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

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

    //全局鎖
    private ReentrantLock lock = new ReentrantLock();

    //記錄線程池大小
    private AtomicInteger currentPoolSize = new AtomicInteger(0);

    public ThreadPoolV8(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;
    }
    static int c = 0;

    public <T> Future<T> submit(Runnable task){
        RunnableFuture<T> ftask = new FutureTask<T>(task, null);
        execute(ftask);
        return ftask;
    }

    public <T> Future<T> submit(Callable<T> task){
        RunnableFuture<T> ftask = new FutureTask<T>(task);
        execute(ftask);
        return ftask;
    }

    //執行任務
    private void execute(Runnable task){
        if(RUNNING){
            /*
                1.當前線程數小於核心線程數時,創建新的工作線程處理
                2.當前線程數等於核心線程數時,加入任務隊列
                3.當任務隊列滿時,創建新的工作線程
                4.當工作線程達到最大線程數時,拒絕提交新的任務
             */
            if(currentPoolSize.get() < corePoolSize && addWorker(task, true)){
                System.out.println("核心線程數:" + (++c));
                //如果隊列滿了,會返回false
            } else if(workerQueues.offer(task)){

            } else if(currentPoolSize.get() < maxPoolSize && addWorker(task, false)){
                System.out.println("非核心線程數:" + (++c));
            } else {
                throw new RuntimeException("線程池已滿,拒絕提交任務");
            }
        }
    }

    //關閉線程池
    public void shutdown(){
        try {
            lock.lock();
            RUNNING = false;
            System.out.println("關閉前線程池大小:" + currentPoolSize);
            for (Worker worker : workers) {
                worker.thread.interrupt();
            }
        } finally {
            lock.unlock();
        }
    }

    //創建新的工作線程
    private boolean addWorker(Runnable task, boolean core){
        for (;;){
            int c = currentPoolSize.get();
            if((core && c < corePoolSize) || !core && c < maxPoolSize){
                if(currentPoolSize.compareAndSet(c, c + 1)){
                    break;
                }
            } else {
                return false;
            }
        }
        try {
            lock.lock();
            Worker w = new Worker(task);
            workers.add(w);
            w.thread.start();
        } finally {
            lock.unlock();
        }
        return true;
    }

    //工作線程類
    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(){
            boolean timeout = false;
            for (;;){
                //如果線程池關閉 並且 工作隊列爲空,那麼可以回收該線程
                if(!RUNNING && workerQueues.isEmpty()) return null;

                try {
                    //如果超時未拿到任務 並且 當前線程數大於核心線程數的時候,就可以回收該線程
                    boolean timed;
                    try {
                        lock.lock();
                        timed = currentPoolSize.get() > corePoolSize;
                        if(timed && timeout) {
                            currentPoolSize.decrementAndGet();
                            return null;
                        }
                    } finally {
                        lock.unlock();
                    }
                    Runnable runnable = timed ? workerQueues.poll(keepTimeAlive, TimeUnit.NANOSECONDS) : workerQueues.take();
                    if(runnable != null){
                        return runnable;
                    }
                    timeout = true;
                } catch (InterruptedException e) {
                    timeout = false;
                }
            }
        }
    }
}

測試代碼1:
同上

測試結果1:
同上

測試代碼2:

public static void main(String[] args) throws ExecutionException, InterruptedException {
    ThreadPoolV6 pool = new ThreadPoolV6(10, 50, 1, TimeUnit.SECONDS, new LinkedBlockingDeque<>(30));
    //ThreadPoolExecutor pool = new ThreadPoolExecutor(10, 50, 1, TimeUnit.SECONDS, new LinkedBlockingDeque<>(30));
    long b = System.currentTimeMillis();
    List<Future<Integer>> futures = new ArrayList<>();
    for (int i = 0; i <= 80; i++) {
        final int start = 1000000 * i, end = 1000000 * (i + 1);
        futures.add(pool.submit(() -> solvePrime(start, end)));
    }
    int res = 0;
    for (Future<Integer> future : futures) {
        res += future.get();
    }
    long e = System.currentTimeMillis();
    System.out.println("結果:" + res);
    System.out.println("耗時:" + (e - b) / 1000.0);
}

static int solvePrime(int start, int end){
    int c = 0;
    for (int i = start; i <= end; i++) {
        c = isPrime(i) ? c + 1 : c;
    }
    return c;
}

static boolean isPrime(int num){
    for (int i = 2; i < num; i++) {
        if(num % 2 == 0){
            return false;
        }
    }
    return true;
}

測試結果2:
V6線程池
在這裏插入圖片描述
java自帶線程池
在這裏插入圖片描述

問題分析:
看測試2,自帶線程居然比V6版效率低?其實在某些場景下,自己實現的線程池效率確實比官方的好,但是穩定性,可用性方面肯定是不如官方的,畢竟官方的實現邏輯非常嚴謹和經過大量測試的。我們來看問題。

  • submit增加了返回值,就是那個熟悉的Future,如果是Runable的任務,get()返回的是null,如果是Callable的任務,就可以返回指定類型的值。

  • 我們重點可以看到addWorker方法,首先返回值從void變成了boolean,這是爲什麼呢?設想一下,如果有10個任務同時提交,那麼execute中的第一行判斷是不是都通過了,當前線程池大小是0,都小於核心線程數,所以addWorker都會進去。看下面這段代碼,先通過CAS將線程池大小+1後,再進行實際的工作線程創建。

    private boolean addWorker(Runnable task, boolean core){
        for (;;){
            int c = currentPoolSize.get();
            //如果是核心線程,那麼當前線程池大小必須小於corePoolSize
            //如果是非核心線程,那麼當前線程池大小必須小於maxPoolSize
            if((core && c < corePoolSize) || !core && c < maxPoolSize){
                //將當前線程池大小+1,之後跳出循環,進行工作線程的創建和添加
                if(currentPoolSize.compareAndSet(c, c + 1)){
                    break;
                }
            } else {
                return false;
            }
        }
        ……
        return true;
    }
    
  • 最後我們再來回顧一下execute方法中的分支判斷

    //在這裏會出現併發addWorker的操作,但是僅僅有corePoolSize個返回true
    if(currentPoolSize.get() < corePoolSize && addWorker(task, true)){
        System.out.println("核心線程數:" + (++c));
        //當前面的addWorker返回false的時候,就會將任務加入隊列中
    } else if(workerQueues.offer(task)){
    
       //這裏也可能出現併發addWorker的操作,但是僅僅有maxPoolSize - corePoolSize個返回true
    } else if(currentPoolSize.get() < maxPoolSize && addWorker(task, false)){
        System.out.println("非核心線程數:" + (++c));
    } else {
        //當隊列滿了,並且最大線程數已經達到了,就會執行這個策略
        throw new RuntimeException("線程池已滿,拒絕提交任務");
    }
    

四、總結
線程池實現到V6版,已經將其核心思想造出來了,接下來不過是對性能的優化和功能性的擴展了。

  • 線程池最核心最核心的思想是,在併發提交任務的場景下,實現對線程的高效管理和複用。
  • 要理解線程池,先從無鎖入手,理解透徹它是如何管理和複用線程去執行新任務的。
  • 要深入理解線程池,需要理解鎖,併發等知識,知道爲什麼需要鎖,哪裏需要鎖,以及如何鎖。
  • 模仿現有的輪子的時候,不要一開始就從最難的開始,應該從最簡單的開始,然後一步一步的去仿造它的核心功能。

最後,關於併發處理,還是官方的🐂🍺,這裏只是將學到的皮毛展示了一下,下一篇將分析java自帶的線程池。

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