通过手撸线程池深入理解其原理(上)

==> 学习汇总(持续更新)
==> 从零搭建后端基础设施系列(一)-- 背景介绍


摘要:源码这东西看着能似懂非懂,有些地方你不知道人家为什么这么设计,过后在想可能又忘了,很没有效率。所以我推荐的学习顺序是看书->看源码->造轮子->总结。这一套下来,花的时间确实多,但是毫不夸张的说,能一劳永逸,那一个个知识点就像印在你脑子里一样。所以这次我从一个最简单的线程池开始,带着每一版遇到的问题,将线程池的各种核心功能逐一给造出来,最后再结合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() + " 结束");
       });
   }
}

测试结果:
通过手撸线程池深入理解其原理(上)_第1张图片

问题分析:
这个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() + " 结束");
       });
   }
}

测试结果:
通过手撸线程池深入理解其原理(上)_第2张图片

问题分析:
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();
    }

测试结果:
通过手撸线程池深入理解其原理(上)_第3张图片

问题分析:
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();
    }
}

测试结果:
通过手撸线程池深入理解其原理(上)_第4张图片

问题分析:
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版的特点。

  • 支持线程池容量设置
  • 支持线程复用,不断执行新任务
  • 支持空闲指定时间的线程自动回收
  • 支持任务排队
  • 支持线程池关闭,清理线程

以上线程池都只支持单线程提交任务,反之则会出现不可预知的结果。

你可能感兴趣的:(php)