1. 首先在项目配置异步线程池,如下:

@EnableAsync // 开启异步任务
@Configuration
public class TaskPoolConfig {
    @Bean("taskExecutor") // 线程池名称
    public Executor taskExecutor() {
        // 使用Spring封装的异步线程池
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(10);               // 初始化线程数
        executor.setMaxPoolSize(20);                // 最大线程数
        executor.setQueueCapacity(200);             // 缓冲队列
        executor.setKeepAliveSeconds(60);           // 允许空闲时间/秒
        executor.setThreadNamePrefix("taskExecutor-");// 线程池名前缀-方便日志查找
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        executor.setWaitForTasksToCompleteOnShutdown(true);
        executor.setAwaitTerminationSeconds(60);
        executor.initialize();   // 初始化
        return executor;
    }

上面我们通过使用ThreadPoolTaskExecutor创建了一个线程池,同时设置了以下这些参数:

  • 核心线程数10:线程池创建时候初始化的线程数
  • 最大线程数20:线程池最大的线程数,只有在缓冲队列满了之后才会申请超过核心线程数的线程
  • 缓冲队列200:用来缓冲执行任务的队列
    允许线程的空闲时间60秒:当超过了核心线程出之外的线程在空闲时间到达之后会被销毁
  • 线程池名的前缀:设置好了之后可以方便我们定位处理任务所在的线程池
  • 线程池对拒绝任务的处理策略:这里采用了CallerRunsPolicy策略,当线程池没有处理能力的时候,该策略会直接在 execute 方法的调用线程中运行被拒绝的任务;如果执行程序已关闭,则会丢弃该任务
    说明:setWaitForTasksToCompleteOnShutdown(true)该方法就是这里的关键,用来设置线程池关闭的时候等待所有任务都完成再继续销毁其他的Bean,这样这些异步任务的销毁就会先于Redis线程池的销毁。同时,这里还设置了setAwaitTerminationSeconds(60),该方法用来设置线程池中任务的等待时间,如果超过这个时候还没有销毁就强制销毁,以确保应用最后能够被关闭,而不是阻塞住。

2.线程池的使用

使用多线程,往往是创建Thread,或者是实现runnable接口,用到线程池的时候还需要创建Executors,spring中有十分优秀的支持,就是注解@EnableAsync就可以使用多线程,@Async加在线程任务的方法上(需要异步执行的任务),定义一个线程任务,通过spring提供的ThreadPoolTaskExecutor就可以使用线程池。

线程池的使用在Spring中非常简单,只要设置两个注解就可以了

(1)@EnableAsync // 开启异步任务

(2)@Async(“taskExecutor”) // 申明为异步方法,指定线程池名称

注: @Async所修饰的函数不要定义为static类型,这样异步调用不会生效

@Slf4j
@Component
public class Task {
 
    public static Random random = new Random();
 
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
 
    @Async("taskExecutor")
    public void doTaskOne() throws Exception {
        log.info("开始做任务一");
        long start = System.currentTimeMillis();
        Thread.sleep(random.nextInt(10000));
        long end = System.currentTimeMillis();
        log.info(stringRedisTemplate.randomKey());
        log.info("完成任务一,耗时:" + (end - start) + "毫秒");
    }
 
    @Async("taskExecutor")
    public void doTaskTwo() throws Exception {
        log.info("开始做任务二");
        long start = System.currentTimeMillis();
        Thread.sleep(random.nextInt(10000));
        long end = System.currentTimeMillis();
        log.info("完成任务二,耗时:" + (end - start) + "毫秒");
    }
 
    @Async("taskExecutor")
    public void doTaskThree() throws Exception {
        log.info("开始做任务三");
        long start = System.currentTimeMillis();
        Thread.sleep(random.nextInt(10000));
        long end = System.currentTimeMillis();
        log.info("完成任务三,耗时:" + (end - start) + "毫秒");
    }
 
}

简单测试如下:

@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest
public class TaskTest {
    @Autowired
    private Task task;
    @Test
    public void test() throws Exception {
        task.doTaskOne();
        task.doTaskTwo();
        task.doTaskThree();
        Thread.currentThread().join();
    }
}

测试结果如下:

2020-04-16 15:23:13.834  INFO 1828 --- [ taskExecutor-1] demo.spring.tasks.Task                   : 开始做任务一
2020-04-16 15:23:13.834  INFO 1828 --- [ taskExecutor-2] demo.spring.tasks.Task                   : 开始做任务二
2020-04-16 15:23:13.835  INFO 1828 --- [ taskExecutor-3] demo.spring.tasks.Task                   : 开始做任务三
2020-04-16 15:23:17.539  INFO 1828 --- [ taskExecutor-2] demo.spring.tasks.Task                   : 完成任务二,耗时:3704毫秒
2020-04-16 15:23:18.380  INFO 1828 --- [   scheduling-1] demo.spring.tasks.ScheduledTasks         : ScheduledTasks1 - The time is now 15:23:18
2020-04-16 15:23:18.381  INFO 1828 --- [   scheduling-1] demo.spring.tasks.ScheduledTasks         : ScheduledTasks2 - The time is now 15:23:18
2020-04-16 15:23:19.475  INFO 1828 --- [ taskExecutor-3] demo.spring.tasks.Task                   : 完成任务三,耗时:5640毫秒

3.配合使用countdownlatch

多用背景:多使用于一个方法内部需要拼接多个方法返回参数最终返回。如:远程调用其他系统接口,担心效率问题。可以采取多线程计数,最后拼装参数返回

未改造前代码如下:

AsyncTask

@Component
public class AsyncTask {

 
    /**
     * 异步调用第三方接口查询
     */
    @Async
    public void queryTask(Entity entity){
       Data data = doPost();
       entity.setData(data);
    }
   
}

service

public class Service {
    
    @Autowired
    private AsyncTask asyncTask;
    @Autowired
    private EntityMapper mapper;
    
    public List<Entity> queryData(){
        
        List<Entity> list = mapper.selectAll();
        for(Entity entity:list){
            asyncTask.queryTask(entity);
        }
        
       return list;
    }
}

这样就实现了异步请求接口,效率上提升了很多。但是,由于异步调用的原因,数据还没填充完就会返回,这显然不是我们想要的效果。我们必须等待AsyncTask中的所有线程结束后,再返回当前调用线程任务的方法

思路:在Service层的方法中实例化CountDownLatch并且制定线程个数,线程个数就是从本地数据库查询的list的长度,并且传入线程任务中,每个线程执行完毕就调用countDown()方法。最后在Service层中调用await()方法。这样在线程计数为零之前,Service的线程就会一直等待

service

public class Service {
    
    @Autowired
    private AsyncTask asyncTask;
    @Autowired
    private EntityMapper mapper;
    
    public List<Entity> queryData(){
        
        List<Entity> list = mapper.selectAll();
        CountDownLatch latch = new CountDownLatch(list.size());
        for(Entity entity:list){
            asyncTask.queryTask(entity,latch);
        }
       latch.await();
       return list;
    }
}

AsyncTask

@Component
public class AsyncTask {
 
 
    /**
     * 异步调用第三方接口查询
     */
    @Async('taskExecutor')
    public void queryTask(Entity entity,CountDownLatch latch){
       Data data = doPost();
       entity.setData(data);
       latch.countDown();
    }
   
}