自己写个小工具,造个轮子 (手写线程池 第一章)

104 阅读6分钟

今天要造的轮子是 线程池

所有的代码已经上传到了github上,大家可以自行下载:github.com/iongst/java…

池化技术这个东西已经不需要多说了,逻辑很简单,正常情况下线程的创建是一个比较耗费资源的事情,能不能用完之后放在某个地方,方便下次继续使用呢? 再简单点就是个缓存而已。

那么我们今天要写的这个小工具就是线程池,本质上和ThreadPoolExcutor 没啥区别,只能说一模一样。

只不过我们会增加一些比较好玩的(其实没啥用)的功能,写代码的目标就 我喜欢就行。

大致希望完成的过程应该包含以下内容:

  • 根据不同的配置文件,去解析读取配置信息;
  • 通过读取的配置信息创建线程池对象;
  • 包含的配置参数有:线程池数量、最大数量、空闲线程存活时间、空闲线程存活时间单位、工作队列、线程工厂、拒绝策略;
  • 在线程池类中实现线程的创建逻辑,可以在构造函数中初始化核心线程,并保存到线程集合中。
  • 实现执行任务的方法execute(),当有新任务到来时,首先判断当前运行的线程数是否达到核心线程数,如果没有达到,就创建一个新线程来执行任务。
  • 如果运行的线程达到了核心线程数,则将任务加入阻塞队列。如果队列已满,则创建新线程来处理任务,但最大线程数不能超过预定的值。
  • 实现线程的关闭与销毁方法,也可以考虑实现定时清理空闲线程的功能。
  • 线程执行完任务后,需要正确的重置其状态,方便线程下次被重用执行任务。
  • 可以通过继承ThreadFactory来实现自定义的线程工厂类,用来创建线程。
  • 设置线程的异常处理,避免线程异常后不可用。

今天要完成的是第一部分,将创建线程池的各个配置项通过配置文件读取进来,然后创建一个对应的Config对象。

大致需要完成的功能如下:

这里涉及到的几个问题:

  • 1: 配置文件中需要配置的信息有哪些?
  • 2: 不同的配置信息读取方式不同;
  • 3: 需要保证用户在使用的时候简单

配置文件中的信息

我们仿照线程池中的内容,全部给他挪过来。

## 核心线程个数
corePoolSize=10
## 最大线程池个数 
maxNumPoolSize=20
## 空闲线程存活时间
keepAliveTime=2
## 时间单位
unit=seconds
## 线程池任务队列
workQueue=linked
## 拒绝策略
handler=AbortPolicy

我们这里少写了一个对应的创建线程的工厂,这个工厂我们放到后面创建的时候手动传入。

针对与这些参数,有几个稍微有点问题的我们说明一下。

针对于时间单位、线程池的任务队列以及拒绝策略我们使用枚举来完成。动态的创建对应的对象。但是因为比如像时间单位和任务队列我们也可以偷懒一下,通过写一些判定条件然后创建指定的对象。等到后面再进行修改。

举个例子:

对于时间单位内容,我们编写一个方法来获取对应的单位值。

// 根据配置的值来创建对应的单位对象,这里直接使用TimeUnit
private TimeUnit getUnit(String value){
    if ("seconds".equalsIgnoreCase(value))
        return TimeUnit.SECONDS;
    else if ("milliseconds".equalsIgnoreCase(value))
        return TimeUnit.MILLISECONDS;
    else
        return  TimeUnit.SECONDS;
}

对于拒绝策略,我们可以直接配置一个枚举,因为这一部分我们在代码中可以自己完成:

//当线程池的任务超出线程池队列可以存储的最大值之后,执行的策略
public enum Policy {
    AbortPolicy, //拒绝并抛出异常
    CallerRunsPolicy,//使用当前调用的线程来执行此任务
    DiscardOldestPolicy,//抛弃队列头部(最旧)的一个任务,并执行当前任务
    DiscardPolicy;//忽略并抛弃当前任务
}

不同的配置信息读取方式不同

我们这里设置了三种对应的配置文件,当然也可以再增加一种,比如json

因为不同的配置文件读取方式不同,所以我们需要创建对应的不同的解析对象;

不同文件的解析方式如下:

  • properties文件可以使用Properties类加载
  • xml文件可以使用DOM,SAX等方式解析
  • yml文件可以使用SnakeYAML等库解析

举例,如果我们解析properties文件,则可以通过下面这个代码完成:

// 通过parser方法来封装一个解析方法 这个方法最后将解析的值封装到Config对象中
public Config parser() {
    properties = new Properties();
    Config config = new Config();
    try {
        properties.load(this.getClass().getClassLoader().getResourceAsStream(configFilePath));
        config.setCorePoolSize(Integer.parseInt(getValue("corePoolSize")));
        config.setMaxNumPoolSize(Integer.parseInt(getValue("maxNumPoolSize")));
        config.setKeepAliveTime(Long.parseLong(getValue("keepAliveTime")));
        config.setUnit(getUnit(getValue("unit")));
        config.setHandler(Policy.valueOf(getValue("handler")));
        config.setWorkQueue(getQueue(getValue("workQueue")));
    } catch (IOException e) {
        throw new IllegalArgumentException("file path is error:"+configFilePath);
    } catch (IllegalArgumentException e){
        throw e;
    }
    return config;
}

需要保证用户在使用的时候简单

保证用户使用简单,目前想来的过程大致分为以下几步:

这里将所有的解析对象全隐藏起来。使用起来如下:

第一步直接获取到一个解析配置对象

这个对象是一个抽象类,它的子类对象就是针对不同的文件格式创建的解析对象;

内部实现细节就是通过后缀名称确定解析对象,并且返回。

// 通过文件名称直接获取到一个解析配置对象
ParserConfig load = ParserConfigSource.load("config.properties");

内部的实现细节如下:

// 创建对应的解析对象
public static ParserConfig load(String configFilePath){
    // 获取后缀
    String extension = getFileExtension(configFilePath);
    if(extension==null||extension.isEmpty()) {
        throw new IllegalArgumentException("Rule config file format is not supported: " + configFilePath);
    }
    // 通过文件名称和后缀 创建对应的对象 放入缓存中
    initCache(configFilePath,extension);
    // 从缓存中直接获取
    return CACHE.get(extension);
}

第二步 通过解析配置对象获取到解析之后的实例对象

// 将数据映射到实体对象中,内部如何实现细节也不需要管了
Config parser = load.parser();

这里的parser方法也是抽象方法,实际会根据load的对象是那个解析对象来具体解析。

// 抽象类中定义的解析方法
abstract Config parser();
// 对于properties中的解析规则如下
public Config parser() {
    properties = new Properties();
    Config config = new Config();
    try {
        properties.load(this.getClass().getClassLoader().getResourceAsStream(configFilePath));
        config.setCorePoolSize(Integer.parseInt(getValue("corePoolSize")));
        config.setMaxNumPoolSize(Integer.parseInt(getValue("maxNumPoolSize")));
        config.setKeepAliveTime(Long.parseLong(getValue("keepAliveTime")));
        config.setUnit(getUnit(getValue("unit")));
        config.setHandler(Policy.valueOf(getValue("handler")));
        config.setWorkQueue(getQueue(getValue("workQueue")));
    } catch (IOException e) {
        throw new IllegalArgumentException("file path is error:"+configFilePath);
    } catch (IllegalArgumentException e){
        throw e;
    }
    return config;
}

为了完成上面的需求,大致设计类图如下:

通过上面的配置,我们基本上就完成了第一部分内容。

如果上面的代码对你来说有点启发,那么不要忘记点个赞、点个在看哦。

版权声明:本站所有文章除特别声明外,均采用 CC BY-NC-ND 4.0 转载请注明来自 kengwanglaoxue
当前文章作者名:kengwanglaoxue
当前文章标题:自己写个小工具,造个轮子 (手写线程池01)
当前文章地址:997coder.com/thread01.ht…