SpringBoot多线程以及线程池使用讲解

HuangLongPu     发表于  2020-12-05 22:29       559

线程池对于Java程序开发已经不模式,用得也比较广泛,下面讲解多线程以及线程池在SpringBoot项目中的使用。

一、默认线程

当我们开启一个SpringBoot的项目,我们并未设置任何和线程相关的操作,但是我们的程序还是可以执行多个请求。甚至说绝大多数的项目,我们不需要对线程这块做任何操作。

但是如果是单线程的话,它显然满足不了我们系统的需求,所有我们有必要了解一下,它默认的线程情况。

1-1、测试默认线程池

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class TestController {

    @GetMapping("/test")
    public String fun(){
        System.out.println(Thread.currentThread().getName());
        return "success";
    }
}

打印结果如下:我请求了14次。

http-nio-8888-exec-1
http-nio-8888-exec-2
http-nio-8888-exec-3
http-nio-8888-exec-4
http-nio-8888-exec-5
http-nio-8888-exec-8
http-nio-8888-exec-7
http-nio-8888-exec-6
http-nio-8888-exec-9
http-nio-8888-exec-10
http-nio-8888-exec-1
http-nio-8888-exec-2
http-nio-8888-exec-3
http-nio-8888-exec-4

可以看到它默认是有10个线程去执行我们的任务的。这个其实就是tomcat的默认线程我们可以在yml/properties里面进行配置。

server:
  port: 8888
  tomcat:
    min-spare-threads: 10
    max-threads: 200

我们可以全局搜索一下这个min-spare-threads,这个json就是Sping的一些默认配置。可以看到里面配置了tomcat默认的线程数是10,最大线程数是200,而对于一般项目来说,这两个数字都已经够用了。


1-2、定时任务默认线程

import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

@Component
@EnableScheduling
public class XdxOne {

    @Scheduled(cron = "*/1 * * * * ?")
    public void testOne() throws Exception {
        System.out.println("one "  + " "+ Thread.currentThread().getName());
    }
}

打印结果如下

one  scheduling-1
one  scheduling-1
one  scheduling-1
one  scheduling-1
one  scheduling-1
one  scheduling-1
one  scheduling-1
one  scheduling-1

我们也可以在上面那个json里面找到这样的一个配置默认的定时任务是一个线程去执行的

{
  "name": "spring.task.scheduling.pool.size",
  "type": "java.lang.Integer",
  "description": "Maximum allowed number of threads.",
  "sourceType": "org.springframework.boot.autoconfigure.task.TaskSchedulingProperties$Pool",
  "defaultValue": 1
}

二、Java线程池核心线程数与最大线程数的区别

2-1、线程池策略

corePoolSize:核心线程数;maximunPoolSize:最大线程数

每当有新的任务到线程池时

第一步:先判断线程池中当前线程数量是否达到了corePoolSize,若未达到,则新建线程运行此任务,且任务结束后将该线程保留在线程池中,不做销毁处理,若当前线程数量已达到corePoolSize,则进入下一步;

第二步:判断工作队列(workQueue)是否已满,未满则将新的任务提交到工作队列中,满了则进入下一步;

第三步:判断线程池中的线程数量是否达到了maxumunPoolSize,如果未达到,则新建一个工作线程来执行这个任务,如果达到了则使用饱和策略来处理这个任务。注意:在线程池中的线程数量超过corePoolSize时,每当有线程的空闲时间超过了keepAliveTime,这个线程就会被终止。直到线程池中线程的数量不大于corePoolSize为止。


由第三步可知,在一般情况下,Java线程池中会长期保持corePoolSize个线程。


2-2、饱和策略

当工作队列满且线程个数达到maximunPoolSize后所采取的策略


AbortPolicy:默认策略;新任务提交时直接抛出未检查的异常RejectedExecutionException,该异常可由调用者捕获。
CallerRunsPolicy:既不抛弃任务也不抛出异常,使用调用者所在线程运行新的任务。
DiscardPolicy:丢弃新的任务,且不抛出异常。
DiscardOldestPolicy:调用poll方法丢弃工作队列队头的任务,然后尝试提交新任务
自定义策略:根据用户需要定制。


三、线程异步执行

在SpringBoot里面异步执行的很简单,只需要加上一个注解就行了 @Async
需要先加上开启异步的注解,在任何地方加上都可以。
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableAsync;

@SpringBootApplication
@EnableAsync
public class App {
    public static void main(String[] args) {
        SpringApplication.run(App.class, args);
    }
}

3-1、加上异步的定时任务执行结果

import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

@Component
@EnableScheduling
@Async
public class XdxOne {

    @Scheduled(cron = "*/1 * * * * ?")
    public void testOne() throws Exception {
        System.out.println("one "  + " "+ Thread.currentThread().getName());
    }
}

异步执行结果

one  task-1
one  task-2
one  task-3
one  task-4
one  task-5
one  task-6
one  task-7
one  task-8
one  task-1
one  task-2
one  task-3

一般任务加上异步

import org.springframework.scheduling.annotation.Async;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@Async
public class TestController {

    @GetMapping("/test")
    public String fun(){
        System.out.println(Thread.currentThread().getName());
        return "success";
    }
}

执行结果

task-1
task-2
task-3
task-4
task-5
task-6
task-7
task-8
task-1

3-3、默认异步线程池

从上面的结果我们可看到,默认的异步线程池的前缀是task-,在SpringBoot2.2.0版本,它的核心线程数是8,这些我们也可以在spring的配置文件里面找到。
{
  "name": "spring.task.execution.pool.core-size",
  "type": "java.lang.Integer",
  "description": "Core number of threads.",
  "sourceType": "org.springframework.boot.autoconfigure.task.TaskExecutionProperties$Pool",
  "defaultValue": 8
},
{
   "name": "spring.task.execution.thread-name-prefix",
   "type": "java.lang.String",
   "description": "Prefix to use for the names of newly created threads.",
   "sourceType": "org.springframework.boot.autoconfigure.task.TaskExecutionProperties",
   "defaultValue": "task-"
 },

3-4、自定义线程池

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.ThreadPoolExecutor.CallerRunsPolicy;


@Configuration
@EnableAsync
public class AsyncConfig {
    
    @Bean(name = "taskExecutor")
    public ThreadPoolTaskExecutor asyncExecutor() {

        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        // 核心线程数
        executor.setCorePoolSize(10);
        // 最大线程数
        executor.setMaxPoolSize(50);
        // 队列最大长度
        executor.setQueueCapacity(1000);
        // 线程池维护线程所允许的空闲时间
        executor.setKeepAliveSeconds(100);
        // 线程前缀
        executor.setThreadNamePrefix("AsyncExecutorThread-");
        // 线程池对拒绝任务(无线程可用)的处理策略
        executor.setRejectedExecutionHandler(new CallerRunsPolicy());
        executor.initialize();
        return executor;
    }
}

我们创建了上面的线程池后,所有的任务都将使用这个线程池里面的线程(可以通过输出线程名查看)


但是如果我们有多个线程池的时候,这个时候我们可以使用@Async(“name”)name就是线程池的名字,没有写name的时候默认使用bean为taskExecutor的线程池。


因为上面我们bean的名字就是taskExecutor,所以我们的其它操作都不用改变,只需要在代码里面加上上面这个文件即可,再次运行之前的代码,我们发现打印的线程就已经是我们自定义的线程池了。

四、总结

  • 默认的任务是使用tomcat默认的线程池去执行,可以在yml/properties里面进行配置。(默认核心线程数:10,最大线程数:200)

  • 默认的定时任务是使用一个单线程去执行。

  • 我们只需要使用@Async和@EnableAsync就可以开启异步执行。

  • 如果没有自定义线程池,将会使用默认的线程池。(SpringBoot的版本不同默认的线程池也不同)

  • 如果自定义了线程池,将会使用我们自定义的线程池。默认使用bean为taskExecuto的线程池。