使用线程池结合短信发送服务

488 阅读4分钟

使用线程池结合短信发送服务

1.1使用线程池的理由

我们为什么要使用线程池?

  • 降低线程创建和销毁的损耗
  • 提高系统的响应速度,当有任务到达的时候,通过复用存在的线程,无需等待新的线程的创建就能立即执行
  • 控制线程数量,控制并发数,如果线程无休止创建,可能会导致内存占用过多产生OOM,且会造成cpu的过度切换(cpu的切换有时间成本)
  • 也可以创建延时定时线程池

1.2线程池使用队列的原因

控制线程无休止的创建,导致内存占用过多造成oom,且导致cpu过度切换

由于线程池创建线程需要获取mainlock这个锁,影响并发,阻塞队列可以做缓冲

1.3线程池为什么使用阻塞队列而不是非阻塞队列

阻塞队列可以让队列为空的时候,获取任务的的线程阻塞,让这个线程进入wait状态,获取cpu资源,当任务中有队列的时候唤醒该线程聪队列中取出消息执行。

让线程不至于一直占用cpu的资源,例如:

while (task != null || (task = getTask()) != null) {
    do something;
}

不用阻塞队列也行,但是比较麻烦

1.4如何配置线程池?

  1. cpu密集型。

    例如:计算量大。尽量使用较小线程池,一般是cpu的核心数+1。cpu密集型让cpu的使用率高,如果线程数目开多了,就会过度切换,得不偿失

  2. IO密集型。

    读写较多。可以使用稍大线程池,一般是2*CPU核心数。因为一般IO密集型的cpu使用率较低,可以让cpu在等待io的时候去做一些其它事情,从而充分利用cpu的时间。

  3. 混合型任务。尽量做任务的拆分,换成前两个,做处理

1.5Sping线程池

Spring利用任务执行器TaskExecutor来实现多线程并发编程。使用ThreadPoolTaskExecutor实现一个基于线程池的TaskExecutor

我们在配置类需要用 @EnableAsync开启异步,并且在异步方法哪里用@Async声明异步任务。

SimpleAsyncTaskExecutor:不是真的线程池,这个类不重用线程,每次调用都会创建一个新的线程。

SyncTaskExecutor:这个类没有实现异步调用,只是一个同步操作。只适用于不需要多线程的地方

ConcurrentTaskExecutor:Executor的适配类,不推荐使用。如果ThreadPoolTaskExecutor不满足要求时,才用考虑使用这个类

SimpleThreadPoolTaskExecutor:是Quartz的SimpleThreadPool的类。线程池同时被quartz和非quartz使用,才需要使用此类

ThreadPoolTaskExecutor :最常使用,推荐。 其实质是对java.util.concurrent.ThreadPoolExecutor的包装

1.6.@Async的事务调度处理

我们可以给大方法加上@Async, 大方法里面调用 A/B两个方法,在这两个方法里面做细粒度的@Transactional,或者用编程式事务。

1.7.配置类的方法说明

Spring 中的ThreadPoolExecutor是借助JDK并发包中的java.util.concurrent.ThreadPoolExecutor来实现的。其中一些值的含义如下:

  • int corePoolSize:线程池维护线程的最小数量
  • int maximumPoolSize:线程池维护线程的最大数量,线程池中允许的最大线程数,线程池中的当前线程数目不会超过该值。如果队列中任务已满,并且当前线程个数小于maximumPoolSize,那么会创建新的线程来执行任务。
  • long keepAliveTime:空闲线程的存活时间TimeUnit
  • unit:时间单位,现由纳秒,微秒,毫秒,秒
  • BlockingQueue workQueue:持有等待执行的任务队列,一个阻塞队列,用来存储等待执行的任务,当线程池中的线程数超过它的corePoolSize的时候,线程会进入阻塞队列进行阻塞等待
  • RejectedExecutionHandler handler 线程池的拒绝策略,是指当任务添加到线程池中被拒绝,而采取的处理措施。
  • 当任务添加到线程池中之所以被拒绝,可能是由于:第一,线程池异常关闭。第二,任务数量超过线程池的最大限制。
  • Reject策略预定义有四种:
  1. ThreadPoolExecutor.AbortPolicy策略,是默认的策略,处理程序遭到拒绝将抛出运行时 RejectedExecutionException
  2. ThreadPoolExecutor.CallerRunsPolicy策略 ,调用者的线程会执行该任务,如果执行器已关闭,则丢弃.
  3. ThreadPoolExecutor.DiscardPolicy策略,不能执行的任务将被丢弃.
  4. ThreadPoolExecutor.DiscardOldestPolicy策略,如果执行程序尚未关闭,则位于工作队列头部的任务将被删除,然后重试执行程序(如果再次失败,则重复此过程)
  5. 自定义策略:当然也可以根据应用场景需要来实现RejectedExecutionHandler接口自定义策略。如记录日志或持久化不能处理的任务

短信服务实战

  1. 做好异步线程池的配置。由于短信发送是IO密集型,所以设置为2*cpu核数
@Configuration
@EnableAsync
public class AsyncConfig {
​
​
    @Bean("smsMessage")
    public Executor sendSmsMessageAsyncExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(4);
        executor.setMaxPoolSize(4);
        executor.setQueueCapacity(100);
        executor.setKeepAliveSeconds(60);
        executor.setThreadNamePrefix("sendSmsMessage-");
        executor.initialize();
        return executor;
    }
}

2.异步方法

 @Override
    @Async("smsMessage")
    public void sendVerifyCodeMessage(String phoneNum) {
        try{
            Client client = AliyunUtil.createClient(accessKeyId, accessKeySecret);
​
            String saveVerifyCode = verifyCodeService.saveVerifyCode(phoneNum);
​
            SendSmsRequest sendSmsRequest = new SendSmsRequest()
                    .setPhoneNumbers(phoneNum)
                    .setSignName(verifyCodeSignName)
                    .setTemplateCode(verifyCodeMessageTemplateId)
                    .setTemplateParam("{'code':'" + saveVerifyCode +"'}")
                    .setPhoneNumbers(phoneNum);
​
            // 复制代码运行请自行打印 API 的返回值
            SendSmsResponse sendSmsResponse = client.sendSms(sendSmsRequest);
            // return saveVerifyCode;
        } catch (Exception e) {
            logger.error("发送失败", e);
            // return "";
        }
    }