专栏首页Java识堂如何手写一个线程池?

如何手写一个线程池?

手写一个异步工具类

我是小识,新来了一个公司。这个公司呢,有个特点,就是很鼓励大家封装各种实用的工具类,提高开发效率。

于是我就到处看项目的源码,看看有没有什么能改进的?果然让我发现了。项目中到处充斥着 new Thread 类来异步执行代码的逻辑

new Thread(r).start();

我们可以封装一个异步工具类啊

第一版

说干就干,把上面的代码简单封装一下,一个简单的异步工具类就封装好了

public interface Executor {

    void execute(Runnable r);
}
public class AsyncExecutorV1 implements Executor {

    @Override
    public void execute(Runnable r) {
        new Thread(r).start();
    }
}

于是开开心心的提交了 merge request

第二版

正当我满怀期待工具类代码能被合并的时候,没想代码被组长杰哥打回来了

「杰哥」:有心封装工具类值得鼓励,不过还可以改进一下

「小识」:还能再改进?没感觉我这个工具类还有改进的余地啊

「杰哥」:假如说有10000个异步任务,你这创建10000个线程,资源耗费太严重了!

「小识」:这样啊,那我加个队列,任务都放到队列中,用一个线程从队列中取任务执行

public class AsyncExecutorV2 implements Executor {

    private BlockingQueue<Runnable> workQueue;

    public AsyncExecutorV2(BlockingQueue<Runnable> workQueue) {
        this.workQueue = workQueue;
        WorkThread workThread = new WorkThread();
        workThread.start();
    }

    @SneakyThrows
    @Override
    public void execute(Runnable r) {
        workQueue.add(r);
    }

    class WorkThread extends Thread {

        @Override
        public void run() {
            while (true) {
                Runnable task = null;
                try {
                    task = workQueue.take();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                task.run();
            }
        }
    }
}

第三版

「小识」:杰哥,快帮我看看,还有啥改进的没?

「杰哥」:小伙子不错啊,居然能想到用队列来缓冲任务,不愧是我招进来的人!但是用一个异步线程执行任务,你确定这个工具类比同步执行的效率快?

「小识」:哈哈,又一个工具类翻车的案例,应该多开几个异步线程来执行任务,但是应该开多少呢?

「杰哥」:谁最清楚异步工具类应该用多少个线程来执行呢?

「小识」:使用工具类的人

「杰哥」:这不对了,你可以定义一个线程数量参数,让用户来决定开多少线程。「另外你这个工具类还个问题,队列满了会直接抛出异常!」

「小识」:那我增加一个拒绝策略类(RejectedExecutionHandler),当线程池满了让用户决定执行策略,比如直接抛异常,用当前线程同步执行任务

public class AsyncExecutorV3 implements Executor {

    private BlockingQueue<Runnable> workQueue;

    private List<WorkThread> workThreadList = new ArrayList<>();

    private RejectedExecutionHandler handler;

    public AsyncExecutorV3(int corePoolSize,
                           BlockingQueue<Runnable> workQueue,
                           RejectedExecutionHandler handler) {
        this.workQueue = workQueue;
        this.handler = handler;
        for (int i = 0; i < corePoolSize; i++) {
            WorkThread workThread = new WorkThread();
            workThread.start();
            workThreadList.add(workThread);
        }
    }

    @SneakyThrows
    @Override
    public void execute(Runnable r) {
        if (!workQueue.offer(r)) {
            // 队列满了,执行拒绝策略
            handler.rejectedExecution(r);
        }
    }

    class WorkThread extends Thread {

        @Override
        public void run() {
            while (true) {
                Runnable task = null;
                try {
                    task = workQueue.take();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                task.run();
            }
        }
    }
}
// 拒绝策略类
public interface RejectedExecutionHandler {

    void rejectedExecution(Runnable r);
}
// 当线程池满了之后直接抛出异常
public class AbortPolicy implements RejectedExecutionHandler {

    @Override
    public void rejectedExecution(Runnable r) {
        throw new RuntimeException("queue is full");
    }
}
// 当线程池满了之后,用提交任务的线程同步执行任务
public class CallerRunsPolicy implements RejectedExecutionHandler {

    @Override
    public void rejectedExecution(Runnable r) {
        r.run();
    }
}

再次提交 merge request,终于被合并了,别的团队都开始使用我的工具类了!开心

过了几天小亮急匆匆找到我

「小亮」:小识,你的工具类挺好用的。但是我最近遇到了一个问题,我用了CountDownLatch批量执行任务,但是我这个任务好像卡住了,我用jstack想看看线程的执行情况,快告诉我你异步线程的名字设置的是啥?

「小识」:哎呀,我们没设置线程的名字,应该用的是默认的线程名字 Thread-n

「小亮」:你可得给工具类加个线程名字的参数啊,不然一个一个看线程的状态太累了,而且效率也不高

「小识」:我这就加

第四版

赶紧加了一个线程名字的参数,然后再次提交代码

「杰哥」:哎呀,没想到我也疏忽了,没发现这个问题,确实应该加个线程名字的参数,代码的可扩展性太重要了,改来改去可不行

「小识」:是啊

「杰哥」:你觉得你只加一个线程名字参数,可扩展性高吗?如果有的团队想修改异步线程的优先级,你再加个优先级参数?

「小识」:感觉不太行,那让用户把线程传给我吧

「杰哥」:哈哈,可以,你还可以用工厂模式优化一下,用户传入线程工厂类,工具类用工厂类创建线程

「小识」:不愧是杰哥,这样一来代码更清爽了!

public class AsyncExecutorV4 implements Executor {

    private BlockingQueue<Runnable> workQueue;

    private List<WorkThread> workThreadList = new ArrayList<>();

    private RejectedExecutionHandler handler;

    public AsyncExecutorV4(int corePoolSize,
                           BlockingQueue<Runnable> workQueue,
                           RejectedExecutionHandler handler,
                           ThreadFactory threadFactory) {
        this.workQueue = workQueue;
        this.handler = handler;
        for (int i = 0; i < corePoolSize; i++) {
         // 用工厂类创建线程
            WorkThread workThread = threadFactory.newThread();
            workThread.start();
            workThreadList.add(workThread);
        }
    }

    @SneakyThrows
    @Override
    public void execute(Runnable r) {
        if (!workQueue.offer(r)) {
            handler.rejectedExecution(r);
        }
    }

    // 异步线程
    public class WorkThread extends Thread {

        @Override
        public void run() {
            while (true) {
                Runnable task = null;
                try {
                    task = workQueue.take();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                task.run();
            }
        }
    }

    // 异步线程工厂类
    public interface ThreadFactory {
        WorkThread newThread();
    }
}

代码提交之后,小亮给线程起了一个名字,async-thread,现在他通过名字很快就能知道线程池中的线程在干嘛了!

大家不断的进行改进

随着这个异步工具类在公司内部使用的越来越多,大家也提交了很多改进的代码

  1. 按需创建线程,不要一开始就创建「corePoolSize」个线程,而是在调用者提交任务的过程中逐渐创建出来,最后创建了「corePoolSize」个就不再创建了
  2. 提高工具的弹性,当任务突增时,队列会被放满,然后多余的任务有可能会被直接扔掉。当然我们可以把「corePoolSize」设的很大,但是这样并不优雅,因为大部分情况下是用不到这么多线程的。当任务突增时,我们可以适当增加线程,提高执行速度,当然创建的总线程数还是要限制一下的,我们把能创建的总数定为「maximumPoolSize」
  3. 及时关闭不需要的线程,当任务突增时,线程数可能增加到「maximumPoolSize」,但是大多数时间「corePoolSize」个线程就足够用了,因此可以定义一个超时时间,当一个线程在「keepAliveTime」时间内没有执行任务,就把它给关掉

异步工具类执行流程图

经过大家的不断改进之后,构造函数中的参数也越来越多了,杰哥让我写个文档吧,把这个异步工具类的构造函数和执行流程总结一下,不然新来的小伙伴看到这个工具类一脸懵可不行!

这个工具类的构造函数目前有如下7个参数

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler)

参数

含义

corePoolSize

核心线程数

maximumPoolSize

最大线程数

keepAliveTime

非核心线程的空闲时间

TimeUnit

空闲时间的单位

BlockingQueue<Runnable>

任务队列

ThreadFactory

线程工厂

RejectedExecutionHandler

拒绝策略

「执行流程图如下」

对了,最后大家给这个异步工具类起了一个牛逼的名字,「线程池」

文章分享自微信公众号:
Java识堂

本文参与 腾讯云自媒体分享计划 ,欢迎热爱写作的你一起参与!

作者:李立敏
原始发表时间:2022-03-09
如有侵权,请联系 cloudcommunity@tencent.com 删除。
登录 后参与评论
0 条评论

相关文章

  • 手写线程池

    从上面我们知道,线程创建,销毁需要时间,线程创建需要占用系统内存。线程的存在是有成本的,这就要看这个成本会不会影响系统性能了。

    Lvshen
  • 手写线程池 - C++版

    在《手写线程池 - C语言版》中,已经实现了 C 语言版的线程池,如果我们也学过 C++ 的话,可以将其改为 C++ 版本,这样代码不管是从使用还是从感观上都会...

    C语言与CPP编程
  • 手写线程池,对照学习ThreadPoolExecutor线程池实现原理!

    作者:小傅哥 博客:https://bugstack.cn Github:https://github.com/fuzhengwei/CodeGuide/wik...

    lbyxiaolizi
  • 线程池理念分析及其手写

    2:提高响应速度,假设线程的创建时间为T1,执行时间为T2,销毁时间为T3,如果是自己创建线程必然会经历,这三个时间,那么如果创建+销毁>执行,就会有大量时间用...

    彼岸舞
  • 死磕 java线程系列之自己动手写一个线程池

    线程池是Java并发编程中经常使用到的技术,那么自己如何动手写一个线程池呢?本文彤哥将手把手带你写一个可用的线程池。

    彤哥
  • 手写线程池 - C语言版

    我们使用线程的时候就去创建一个线程,这样实现起来非常简便,但是就会有一个问题:如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创...

    C语言与CPP编程
  • 手写改造线程池和拒绝策略

    名字是乱打的
  • 并发编程之手写一个简单的线程池

    有些人可能对线程池比较陌生,并且更不熟悉线程池的工作原理。所以他们在使用多线程的时候,往往都是通过直接new Thread来实现多线程。但是往往良好多线程的设计...

    全栈程序员站长
  • 死磕 java线程系列之自己动手写一个线程池(续)

    上一章我们自己动手写了一个线程池,但是它是不支持带返回值的任务的,那么,我们自己能否实现呢?必须可以,今天我们就一起来实现带返回值任务的线程池。

    彤哥
  • 线程池:第三章:线程池的手写改造和拒绝策略以及线程池配置合理线程数

    我们线程池使用ThreadPoolExecutor的方式进行创建,下面看底层源码:

    Java廖志伟
  • 如何创建线程池

    中强制线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规...

    崔笑颜
  • 面试题 -- 如何设计一个线程池

    这是一个常见的问题,如果在比较熟悉线程池运作原理的情况下,这个问题并不难。设计实现一个东西,三步走:是什么?为什么?怎么做?

    秦怀杂货店
  • 面经手册 · 第21篇《手写线程池,对照学习ThreadPoolExecutor线程池实现原理!》

    正好是2020年,看到这张图还是蛮有意思的。以前小时候总会看到一些科技电影,讲到机器人会怎样怎样,但没想到人似乎被娱乐化的东西,搞成了低头族、大肚子!

    小傅哥
  • 面试题 -- 如何设计一个线程池

    这是一个常见的问题,如果在比较熟悉线程池运作原理的情况下,这个问题并不难。设计实现一个东西,三步走:是什么?为什么?怎么做?

    秦怀杂货店
  • 由浅入深理解Java线程池及线程池的如何使用

    前言 多线程的异步执行方式,虽然能够最大限度发挥多核计算机的计算能力,但是如果不加控制,反而会对系统造成负担。线程本身也要占用内存空间,大量的线程会占用内存资源...

    Janti
  • 如何合理使用线程池?

    创建线程池要使用手动方式,自动创建线程使用newFixedThreadPool和newCachedThreadPool可能因为资源耗尽导致OOM问题。

    关忆北.
  • python线程池如何使用

    线程池的基类是 concurrent.futures 模块中的 Executor,Executor 提供了两个子类,即 ThreadPoolExecutor 和...

    砸漏
  • 线程池如何传递ThreadLocal

    在做分布式链路追踪系统的时候,需要解决异步调用透传上下文的需求,特别是传递traceId,本文就线程池透传几种方式进行分析。

    朱可道
  • NioEventLoop 是一个线程的线程池

    我们现在知道, 当一个新的客户端连接到服务器时, 通过选择器EventExecutorChooser选择一个NioEventLoop为其服务. 那么其实最终是由...

    乐事

扫码关注腾讯云开发者

领取腾讯云代金券