Java线程池详解,内含实战演练~

 本文是向大家介绍线程池的使用和一些注意事项,它能够实现高并发下快速处理业务,能够帮助开发人员深入理解线程池的价值。


1. 简介

线程池是使用池化技术管理和使用线程的一种机制。池化技术:提前准备一些资源,在需要时可以重复使用使用提前准备的资源。常见的有内存池、数据库连接池等。

2. 参数说明

  1. 需要如下参数:

corePoolSize:核心线程数。默认情况下,核心线程会一直存活,但是当将 allowCoreThreadTimeout 设置为 true 时,核心线程也会超时回收。

maximumPoolSize:线程池所能容纳的最大线程数。当活跃线程数达到该数值后,后续的新任务将会阻塞。

keepAliveTime:线程闲置超时时长。如果超过该时长,非核心线程就会被回收。

unit:指定 keepAliveTime 参数的时间单位。常用的有:TimeUnit.MILLISECONDS(毫秒)、TimeUnit.SECONDS(秒)、TimeUnit.MINUTES(分)。

workQueue:任务队列。通过线程池的 execute() 方法提交的 Runnable 对象将存储在该参数中。其采用阻塞队列实现。

threadFactory(可选):线程工厂。用于指定为线程池创建新线程的方式。

handler(可选):拒绝策略。当达到最大线程数时需要执行的饱和策略。

  1. 参数配置异常
if (corePoolSize < 0 ||maximumPoolSize <= 0 ||maximumPoolSize < corePoolSize ||
    keepAliveTime < 0){
    throw new IllegalArgumentException();
}            

3. 使用顺序

核心线程满->任务队列满->最大线程满->拒绝处理

一个线程池所能承载最大线程量:任务队列大小+最大线程数

4. 默认线程工厂

Executors 框架已经为我们实现了默认的线程池:

线程池

最大线程数

任务队列

应用场景

FixedThreadPool

自定义

LinkedBlockingQueue

控制线程最大并发数,cpu比较平稳

ScheduledThreadPool

Integer.MAX_VALUE

DelayedWorkQueue

执行定时或周期性的任务

CachedThreadPool

Integer.MAX_VALUE

SynchronousQueue

执行大量、耗时少的任务

SingleThreadExecutor

1

LinkedBlockingQueue

单线程的线程池,保证执行顺序

总结:

  1. FixedThreadPool SingleThreadExecutor:主要问题是堆积的请求处理队列均采用 LinkedBlockingQueue,而LinkedBlockingQueue未指定容量,默认大小是Integer.MAX_VALUE,可能会耗费非常大的内存导致 OOM。任务队列无界时,设置最大线程数是没有意义的。
  2. CachedThreadPool ScheduledThreadPool:主要问题是线程最大数是 Integer.MAX_VALUE,可能会创建数量非常多的线程导致 OOM。
  3. Executors 的 4 个功能线程池虽然方便,但不建议使用,而是建议直接通过使用 ThreadPoolExecutor 的方式,规避资源耗尽的风险

5. 正确打开方式

ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
    //核心线程数
    1,
    //最大线程数
    1,
    //超时时间
    1L,
    //时间单位
    TimeUnit.MILLISECONDS,
    //任务队列
    new LinkedBlockingDeque<Runnable>(1),
    //线程工厂
    new MyThreadFactory(),
    //拒绝策略
    new MyThreadRunsPolicy());
  1. 指定最大线程数
  2. 任务队列指定大小
  3. 线程工厂自定义

参考DefaultThreadFactory,自定义MyThreadFactory只修改了名字前缀,方便查询日志

public class MyThreadFactory 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;

    MyThreadFactory() {
        SecurityManager s = System.getSecurityManager();
        group = (s != null) ? s.getThreadGroup() :
                Thread.currentThread().getThreadGroup();
         //此处只修改了一下名字前缀      
        namePrefix = "业务1-" +
                poolNumber.getAndIncrement() +
                "-thread-";
        System.out.println("--->"+namePrefix);
    }
    @Override
    public Thread newThread(Runnable r) {
        Thread t = new Thread(group, r,
                namePrefix + threadNumber.getAndIncrement(),
                0);
        if (t.isDaemon()){
            t.setDaemon(false);
        }
        if (t.getPriority() != Thread.NORM_PRIORITY){
            t.setPriority(Thread.NORM_PRIORITY);
        }
        return t;
    }
}
  1. 自定义拒绝策略
public class MyThreadRunsPolicy implements RejectedExecutionHandler {
        MyThreadRunsPolicy() {
            super();
        }

        @Override
        public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
            try {
                final Thread t = new Thread(r, "Temporary task executor");
                t.start();
            } catch (Throwable e) {
                throw new RejectedExecutionException("Failed to start a new thread", e);
            }
        }
    }

Executors 框架已经为我们实现了 4 种拒绝策略:

  1. AbortPolicy(默认):丢弃任务并抛出 RejectedExecutionException 异常。
  2. CallerRunsPolicy:由调用线程处理该任务,
  3. DiscardPolicy:丢弃任务,但是不抛出异常。
  4. DiscardOldestPolicy:丢弃队列最早的未处理任务,然后重新尝试执行任务。

6. springboot集成线程池

  1. 全局线程池配置
@Configuration
@EnableAsync // 利用@EnableAsync注解开启异步任务支持
public class CustomMultiThreadingConfig {
    @Bean("threadPoolTaskExecutor")
    public Executor getAsyncExecutor() {
        ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
        // 核心线程数
        taskExecutor.setCorePoolSize(10);
        // 最大线程数
        taskExecutor.setMaxPoolSize(20);
        //配置队列大小
        taskExecutor.setQueueCapacity(10);
        // 配置线程池前缀
        taskExecutor.setThreadNamePrefix("async-service-");
        // 配置拒绝策略
        taskExecutor.setRejectedExecutionHandler(new MyThreadRunsPolicy());
        // 数据初始化
        taskExecutor.initialize();
        return taskExecutor;
    }

    private static final class MyThreadRunsPolicy implements RejectedExecutionHandler {
        MyThreadRunsPolicy() {
            super();
        }

        @Override
        public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
            try {
                final Thread t = new Thread(r, "Temporary task executor");
                t.start();
            } catch (Throwable e) {
                throw new RejectedExecutionException("Failed to start a new thread", e);
            }
        }
    }
}
  1. 待线程池执行任务添加@Async("threadPoolTaskExecutor")
@Component
public class AsyncSubTaskExecutorServiceImpl implements AsyncSubTaskExecutorService {
    @Async("threadPoolTaskExecutor")
    @Override
    public void extracted() {
        
    }
}

注意:异步方法A调用异步方法B时,A和B不能在同一类中,否则无效

7. 浅谈设置线程池大小

7.1 计算线程池最优大小的公式:

W/C 称为阻塞系数,对于CPU密集型任务阻塞系数为0,cpu核的数量就是线程数,拥有更多的线程数也是用处不大的。也可以得到“线程等待时间所占比例越高,需要越多线程”。

7.1.1 获取核数

16个CPU,1核,所以有16个逻辑CPU

7.1.2 获取等待输入输出的CPU时间百分比

"top"命令查看cup详情

按“1”键查看所有cpu

wa 等待输入输出的CPU时间百分比,CPU等待磁盘IO操作的时间

us 用户空间占用CPU百分比,比如shell程序、各种语言的编译器、数据库应用、web服务器和各种桌面应用等;

sy 内核空间占用CPU百分比,系统资源都是由Linux内核处理,比如分配一些内存、或是执行IO操作、再或者是去创建一个子进程。

7.2 实战:reconciliationweb结算同步任务线程数调试

1. 根据并发高低和任务执行时间来估算线程数

并发

任务执行时间

业务类型

估算

cpu核数+1,减少线程上下文切换

使用中间件等加速执行时间

IO密集型:

文件传输,网络请求等,cpu需等待结果后执行下一步

cpu核数*2+1

CPU密集型:

递归计算等

cpu核数+1

cpu核数+1

通过linux命令查看reconciliationweb服务核数为16,结算同步业务仅商务人员操作,并发低,但数据量大,数据量达400万,调用支付宝接口执行时间长,属于IO密集型,那么线程数起点可以配置33来调试。

2. 查看reconciliationweb服务分配核数

查看服务分配2核cpu,8g内存;

3. k8s上查看cpu使用情况

通过不断调试核数,来查看cpu使用的变化趋势,在满足平均cpu使用率不超过80%的情况下找到接近最优解的值;

猜你喜欢

转载自blog.csdn.net/weixin_43805705/article/details/130359051