自定义线程池

198 阅读6分钟

一、、ThreadPoolExecutor引入

1.1 什么是线程池

线程池,thread pool,是一种线程使用模式,线程池维护着多个线程,等待着监督管理者分配可并发执行的任务。

通俗来说,就是可管理和维护以及分配线程的“池子”。

1.2 为什么使用线程池?

为了减少创建和销毁线程的次数,让每个线程都可以多次的使用,可以根据系统情况调整线程的数量,防止消耗过多内存。在实际使用中,服务器在创建和销毁线程上花费的时间和消耗的系统资源都相当大,使用线程池就可以优化。

他的主要特点为:  线程复用、控制最大并发数、管理线程。

  • 降低资源消耗。通过重复利用自己创建的线程降低线程创建和销毁造成的消耗。

  • 提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。

  • 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗资源,还会降低系统的稳定性,使用线程池可以进行统一分配,调优和监控。

1.3 核心参数

public ThreadPoolExecutor(int corePoolSize,//核心线程数
                          int maximumPoolSize,//最大线程数
                          long keepAliveTime,//线程空闲时间
                          TimeUnit unit,//时间单位
                          BlockingQueue<Runnable> workQueue,//任务队列
                          ThreadFactory threadFactory,//线程工厂
                          RejectedExecutionHandler handler//拒绝策略) 
{
    ...
}
  • corePoolSize: 一直保持的线程的数量,即使线程空闲也不会释放。除非设置了 allowCoreThreadTimeout 为 true;
  • maximumPoolSize:允许最大的线程数,队列满时开启新线程直到等于该值;

1、如果没有空闲的线程执行该任务且当前运行的线程数少于 corePoolSize,则添加新的线程执行该任务。

2、如果没有空闲的线程执行该任务且当前的线程数等于corePoolSize同时阻塞队列未满,则将任务入队列,而不添加新的线程。

3、如果没有空闲的线程执行该任务且阻塞队列已满同时池中的线程数小于maximumPoolSize,则创建新的线程执行任务。

4、如果没有空闲的线程执行该任务且阻塞队列已满同时池中的线程数等于maximumPoolSize,则根据 handler 指定的策略来拒绝新的任务。

  • keepAliveTime:表示空闲线程的存活时间。

    • 当线程空闲时间达到keepAliveTime,该线程会退出,直到线程数量等于corePoolSize
      只有当线程池中的线程数大于corePoolSizekeepAliveTime才会起作用,直到线程中的线程数不大于corepoolSIze
  • TimeUnitunit:表示keepAliveTime的单位。

  • workQueue:缓存任务的队列。实现 BlockingQueue

    • LinkedBlockingQueue<Runnable>:用链表实现的队列,可以是有界的,也可以是无界的,但在Executors中默认使用无界的。
  • handler:表示当 workQueue 已满,且池中的线程数达到 maxPoolSize 时,线程池拒绝添加新任务时采取的策略。

1.ThreadPoolExecutor.AbortPolicy():默认值;抛出RejectedExecutionException异常 2.ThreadPoolExecutor.CallerRunsPolicy():由向线程池提交任务的线程来执行该任务 3.ThreadPoolExecutor.DiscardOldestPolicy():抛弃最旧的任务(最先提交而没有得到执行的任务)

4.ThreadPoolExecutor.DiscardPolicy():抛弃当前的任务

  • threadFactory:指定创建线程的工厂。
    • 创建工厂的两种方式:(未指定时默认)Executors.defaultThreadFactory() 、new ThreadFactoryBuilder().setNameFormat("task-service-pool-%d").build()

    • 为了统一在创建线程时设置一些参数,如是否守护线程,线程一些特性等,如优先级。通过这个TreadFactory创建出来的线程能保证有相同的特性。

    • 如果没有另外说明,则在同一个ThreadGroup 中一律使用Executors.defaultThreadFactory() 创建线程,并且这些线程具有相同的NORM_PRIORITY 优先级和非守护进程状态

一个额外参数:

  • allowCoreThreadTimeout:是否允许核心线程空闲退出,默认值为false。
    核心线程在 allowCoreThreadTimeout 为 true 时才会超时退出,默认不会退出。

二、问题展现:为什么要自定义线程池

说明: 我们创建一个线程池的配置类,将所有线程池都写在其中,利于我们在业务中直接提交任务到我们的线程池!

注意: 由于每个模块需要创建的自定义线程池不一样,而且有着自己独特需求的资源量(核心线程和最大线程),所以我们选择在业务模块中创建自定义线程池,而不是抽取成为一个共用模块!

配置类:

package com.ssm.user.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

@Configuration
public class ThreadPollConfig {

     @Bean(name = "mailThreadPool")
     public ThreadPoolExecutor getMailThreadPool() { //发送邮件的线程池

         return new ThreadPoolExecutor(
                 20, //核心线程数
                 50, //最大线程数
                 5, //空闲线程存活时间
                 TimeUnit.SECONDS, //单位
                 new LinkedBlockingDeque<>(), //阻塞队列
                 new ThreadPoolExecutor.CallerRunsPolicy() //拒绝策略(由向线程池提交任务的线程来执行该任务)
         );
         //未声明线程工厂,默认为Executors.defaultThreadFactory
     }
}

测试类:

package com.ssm.user;

import lombok.extern.slf4j.Slf4j;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

import javax.annotation.Resource;
import java.util.concurrent.ThreadPoolExecutor;

@SpringBootTest(classes = UserApplication.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@Slf4j
@RunWith(SpringRunner.class)
public class MailThreadPoolTest {

    @Resource(name = "mailThreadPool")
    private ThreadPoolExecutor mailThreadPool;

    @Test
    public void test() {
        for (int i = 0; i < 10; i ++) {
            mailThreadPool.submit(new Runnable() {
                @Override
                public void run() {
                    log.info("当前时间为:{}", System.currentTimeMillis());
                }
            });
        }
    }

}

image.png

结果: 如果我们项目中使用了大量的线程池,但是在日志中,这些大量线程池都只会打印ThreadPool,这就导致了具体是哪个业务线使用了哪个线程池,我们都是不知道的,所以需要自定义线程池,要对线程池的名称进行自己的独立定义,这样在日志中便能够看出线程池属于哪个业务线了!

三、自定义线程工厂

新增ape-common-threadpool模块,创建config.CustomNameThreadFactory类

我们可以根据默认的线程工厂Executors.defaultThreadFactory()进行仿写!

image.png

线程工厂类:

package com.ssm.threadpool.config;

import java.util.concurrent.ThreadFactory;
import java.util.concurrent.atomic.AtomicInteger;

public class CustomNameThreadFactory implements ThreadFactory {

    private static final AtomicInteger poolNumber = new AtomicInteger(1);
    private final ThreadGroup group;
    private final AtomicInteger threadNumber = new AtomicInteger(1);
    private final String namePrefix;

    public CustomNameThreadFactory(String threadName) { //构造函数中传入name作为标识
        SecurityManager s = System.getSecurityManager();
        group = (s != null) ? s.getThreadGroup() :
                Thread.currentThread().getThreadGroup();
        namePrefix = threadName +
                poolNumber.getAndIncrement() +
                "-thread-"; //默认的是由pool拼接,我们改为自己的name
    }

    @Override
    public Thread newThread(Runnable r) {
        // 创建相应的Thread任务
        Thread t = new Thread(group, r,
                namePrefix + threadNumber.getAndIncrement(),
                0);


        // 检查是否为守护线程
        if (t.isDaemon()) {
            // 如果是守护线程,则设置为非守护线程
            t.setDaemon(false);
        }

        // 判断线程的优先级是否为默认优先级5(可根据业务调整)
        if (t.getPriority() != Thread.NORM_PRIORITY) {
            // 如果不是,则将该线程设置为默认优先级
            t.setPriority(Thread.NORM_PRIORITY);
        }

        return t;
    }
}

线程池配置类:

package com.ssm.user.config;

import com.ssm.threadpool.config.CustomNameThreadFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

@Configuration
public class ThreadPollConfig {

     @Bean(name = "mailThreadPool")
     public ThreadPoolExecutor getMailThreadPool() { //发送邮件的线程池
     
         // 创建自定义的线程工厂(用于自定义线程名)
         CustomNameThreadFactory mailFactory = new CustomNameThreadFactory("mail");
         return new ThreadPoolExecutor(
                 20, //核心线程数
                 50, //最大线程数
                 5, //空闲线程存活时间
                 TimeUnit.SECONDS, //单位
                 new LinkedBlockingDeque<>(), //阻塞队列
                 mailFactory, //添加自定义的mail线程工厂
                 new ThreadPoolExecutor.CallerRunsPolicy() //拒绝策略(由向线程池提交任务的线程来执行该任务)
         );
         //未声明线程工厂,默认为Executors.defaultThreadFactory
     }
}

测试类:

package com.ssm.user;

import lombok.extern.slf4j.Slf4j;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

import javax.annotation.Resource;
import java.util.concurrent.ThreadPoolExecutor;

@SpringBootTest(classes = UserApplication.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@Slf4j
@RunWith(SpringRunner.class)
public class MailThreadPoolTest {

    @Resource(name = "mailThreadPool")
    private ThreadPoolExecutor mailThreadPool;

    @Test
    public void test() {
        for (int i = 0; i < 10; i ++) {
            mailThreadPool.submit(new Runnable() {
                @Override
                public void run() {
                    log.info("当前时间为:{}", System.currentTimeMillis());
                }
            });

        }
    }

}

image.png

结论: 可以看到,不再是像默认的pool-1-thread-1这种了,而能看出来是使用了mail这个线程池!