多线程在Spring Boot中的应用

一、前言

在操作系统中,线程无法独立存在,必须归属于进程的一部分,可以并发执行多个来完成任务,共享代码和数据空间。多线程是多任务的一种特别形式,借助资源共享的特点,可以充分利用CPU,在同一时间内运行更多不同种类的任务,节省资源的开销。

二、Java多线程概述

一个线程在全生命周期内有五种状态,状态之间的转换过程如下图所示。

 

  • 新建状态:新创建了一个线程对象。

  • 就绪状态:创建后,其他线程调用了该对象的start()方法,该状态的线程位于可运行的线程池中,等待JVM线程调度器的调度。

  • 运行状态:获取到CPU资源的线程执行run()方法,线程处于运行状态,该状态可转变为阻塞、就绪、死亡状态。

  • 阻塞状态:执行wait()方法,JVM会把该运行中的线程放入等待池中 执行过程中获取对象的同步锁,若被其他线程占用,则会被JVM放入锁池 执行sleep()方法后,JVM会把该线程置为阻塞状态,当sleep()超时后重新转入就绪状态,执行join()方法或发出I/O请求效果同样。

  • 死亡状态:线程执行完毕或异常退出run()方法,线程生命周期结束。

     

Java程序中一般通过三种方式实现多线程:继承Thread类、实现Runnable接口和实现Callable接口,其中前两种方式无返回值,第三种方式是有返回值的(通过调用FutureTask对象的get方法来获取),下面是实现的简单实例。

继承Thread类

public class LearningThread extends Thread{
    private String thread;
    LearningThread(String thread){
        this.thread = thread;
    }
    public void run(){
        for (int i = 0;i<10;i++){
            System.out.println("这是"+thread+"线程在运行:"+i);
        }
    }
}
public class Main{
    public static void main(String[] args){
        new Thread(new LearningThread("1号")).start();
        new Thread(new LearningThread("2号")).start();
    }
}

实现Runnable接口

public class LearningThread implements Runnable{
    private String thread;
    LearningThread(String thread){
        this.thread = thread;
    }
    @override
    public void run(){
        for (int i = 0;i<10;i++){
            System.out.println("这是"+thread+"线程在运行:"+i);
        }
    }
}
public class Main{
    public static void main(String[] args){
        new Thread(new LearningThread("1号")).start();
        new Thread(new LearningThread("2号")).start();
    }
}

实现Callable接口

public class LearningThread implements Callable<String> {
    private String thread;
    LearningThread(String thread){
        this.thread = thread;
    }
    public String call() {
        for (int i = 0;i<10;i++){
            System.out.println("这是"+thread+"线程在运行:"+i);
        }
        return thread+":"+Thread.currentThread().getName();
    }
}
public static void main(String[] args) {
    LearningThread lt = new LearningThread("Callable接口3号");
    FutureTask<String> ft = new FutureTask<String>(lt);
    new Thread(ft,"有返回值的线程").start();
    try{
        System.out.println("线程返回值:"+ft.get());
    }catch (Exception e){
        e.printStackTrace();
    }
}

三种实现方式各不相同,可根据需求来选择,但细看无返回值的两种实现方式中的前者,Thread类结构如下:

class Thread implements Runnable 

可以发现Thread类实现了Runnable接口,它们之间具有多态关系。在使用继承Thread类方式创建新线程时,局限是无法支持多继承。因此为了支持多继承,可以采用实现Runnable接口的方式。但两种方式创建的线程在工作时的性质完全一样,无本质的区别。

在上面三个例子中可以看出,每次执行一个任务均需要新建一个Thread的对象,而线程的创建与销毁是非常耗费系统资源,若在任务过多的状态,花在创建和销毁线程上的时间会比线程真正执行的时间还长,还可能会造成线程之间的相互竞争,占用过多的系统资源,最终导致内存泄露。

理所当然的,Java提供了线程池来统一管理线程的创建与销毁和系统资源的分配,具备定时执行、并发执行等功能,最大程度的避免上述情况。在Java中,Executor支持多种不同类型的任务执行策略,提供了对线程生命周期的支持等机制。最常用的则是用户创建各种线程池的Executors类和ThreadPoolExecutor线程池。其中Executors类提供给了一系列的静态工厂方法,常用的有以下几种:

  • newCachedThreadPool 可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。

  • newFixedThreadPool 定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。

  • newScheduledThreadPool 定长线程池,支持定时及周期性任务执行。

  • newSingleThreadExecutor 单线程化的线程池,它只会用唯一的工作线程来执行任务,若线程异常结束则会重新启动一个线程来代替原有的线程继续执行。

ThreadPoolExecutor通过调用Executors的某个静态工厂方法来创建线程池,并返回一个ExecutorService对象,此时线程池处于运行状态,直至shutdown()方法被执行。

三、SpringBoot多线程应用

SpringBoot通过ThreadPoolTaskExecutor类实现线程池管理,而这个类对java.util.concurrent.TreadPoolExector进行了封装,在初始化有几个基本参数。

private int corePoolSize = 1;//核心线程数,默认1
private int maxPoolSize = 2147483647;//最大线程数
private int keepAliveSeconds = 60;//某线程空闲超过这个时间,就回收该线程
private int queueCapacity = 2147483647;//队列大小
private boolean allowCoreThreadTimeOut = false;//主线程超时,默认为false
private TaskDecorator taskDecorator;
private ThreadPoolExecutor threadPoolExecutor;//线程池方法类

显然默认参数值并不适用于大多数场景,既是Springboot的多线程应用,参数值的配置放在配置文件中更合适,实例中选择了application.yml文件,相关参数配置如下:

thread:  
  corePoolSize: 2
  maxPoolSize: 4
  queueCapacity: 5
  keepAlive: 60

线程池的初始化也交由Bean容器来实现,不需要调用构造方法,在使用时通过在方法上增加@Async("方法名")注解即可交由该线程池管理。

@Bean
   public Executor springExecutor() {
       ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
       executor.setCorePoolSize(catchCorePoolSize);
       executor.setMaxPoolSize(catchMaxPoolSize);
       executor.setQueueCapacity(catchQueueCapacity);
       executor.setThreadNamePrefix("consumerExecutor-");
       executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); //当pool已经达到max size的时候,如何处理新任务
       executor.setKeepAliveSeconds(catchKeepAlive);
       executor.initialize();
       return executor;
   }
}

上面在初始化线程池时配置了6个参数,具体解释如下:

  • CorePoolSize 核心线程数,即线程池活跃的线程数,新任务过来时会直接申请该线程,不需要重新创建,生命周期跟线程池一致。

  • MaxPoolSize 最大线程数,当核心线程数和阻塞队列都满了的时候,线程池会创建新的线程去处理新任务,当线程空闲到KeepAliveSeconds时间后会自动销毁。

  • QueueCapacity 队列大小,当核心线程资源耗尽后,线程池会将新任务放置在阻塞队列中,待有空闲线程时再去消费。

  • RejectedExecutionHandler 当线程池的资源耗尽时,如何处理新任务申请线程的需求,默认为拒绝新任务并抛出。java.util.concurrent.RejectedExecutionException异常(也可以委托Main线程来帮忙执行新任务)。

  • ThreadNamePrefix 线程名称前缀,可以标识当前线程的名称。

  • KeepAliveSeconds 当多余核心线程数的线程空闲多久时间时会释放掉。

使用时需要关注以下几点:

  1. 方法直接调用即可,此时需要在执行方法的主程序中增加类注解@EnableAysnc;

  2. 交由线程池管理方法和执行该任务的方法不可以在同一个类中;

  3. 多个方法可以共享一个线程池,@Aysnc注解中的值一样即可。

 

下面提供一个简单的例子来测试线程池各个参数的应用,配置不变,for循环中提交了10个任务交给线程池,而线程池配置最大线程数为4,阻塞队列大小是5,饱和策略选择了默认丢弃并抛出未检查的异常,因此会有一个任务会出现无法处理的情况,根据上面提到的特性,若同时提交会抛出线程资源耗尽的异常,若前置任务有提交完成的,则能顺利执行。

   @Async("springExecutor")
    public void testRun(){
        System.out.println("当前执行线程为"+Thread.currentThread().getName() +",执行了"+ i);
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            System.out.println("休眠失败"+e.getMessage());
        }
    }
@EnableAsync
@Component
public class ApplicationThread implements InitializingBean{
    @Autowired
    private LearningThread thread;
    public void afterPropertiesSet() {
       for (int i = 1 ; i< 11;){
            //异步执行任务,每个耗时20ms
            thread.springRun(i);
            //输出应用启动后所有的线程名称
            printAllThreads(Thread.getAllStackTraces());
            i++;
            if (i == 10) Thread.sleep(20);//第十个超过线程池容量,等待20ms等前置任务处理完毕,若不等待则会抛出异常
        }
    }
}

结果如下图所示。

在这里让最后一个任务等待若干时间可顺利执行,无等待时因配置了新线程处理策略为丢弃并抛出异常,控制台输出的错误日志。

从执行的结果可以看出,线程1和2执行了前两个任务,3~7因为核心线程满了被放入了阻塞队列,8和9任务进来时因队列已满,线程池判断现有的配置无法满足工作需求,新建了2个线程来处理新任务,而阻塞队列里的任务等待线程释放后才被消费。 此外,SpringBoot也提供了有返回的异步回调功能,通过Future类来返回异步回调的结果,异步方法可修改如下:

@Async("springExecutor")
public Future<String> asyncTask(){
    System.out.println("这是一个有返回值的任务");
    Future<String> future = new AsyncResult<String>("返回值");
    return future;
}

本文主要是对前段时间学习和工作实践遇到知识点的学习和积累,过程中参考了技术博客与书籍,欢迎阅读的各位批评指正。

  • 0
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Spring Boot支持多种方式实现多线程,以下是其的一些: 1. 继承Thread类 可以创建一个类,继承Thread类,并重写run()方法实现多线程。然后在Spring Boot应用创建该类的实例,并调用start()方法启动线程。 2. 实现Runnable接口 可以创建一个类,实现Runnable接口,并实现run()方法实现多线程。然后在Spring Boot应用创建该类的实例,并将其作为参数传递给Thread类的构造函数,最后调用start()方法启动线程。 3. 使用Java Executor框架 Java Executor框架是Java多线程编程的一个重要框架,它提供了一系列的线程池和任务调度的API,可以方便地实现多线程编程。在Spring Boot应用,可以使用Java Executor框架来创建线程池,然后提交任务实现多线程。 4. 使用Spring TaskExecutor Spring TaskExecutor是Spring框架提供的一个任务执行器,它基于Java Executor框架实现,可以方便地实现多线程编程。在Spring Boot应用,可以使用Spring TaskExecutor来创建线程池,然后提交任务实现多线程。 5. 使用Spring @Async注解 Spring框架提供了@Async注解,可以将一个方法标记为异步执行,该方法会在一个独立的线程执行。在Spring Boot应用,可以使用@Async注解来实现多线程编程。需要在配置类开启@EnableAsync注解,然后在需要异步执行的方法上加上@Async注解。 总之,Spring Boot提供了多种方式来实现多线程编程,开发人员可以根据自己的需求和习惯选择合适的方式。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值