一、前言
在操作系统中,线程无法独立存在,必须归属于进程的一部分,可以并发执行多个来完成任务,共享代码和数据空间。多线程是多任务的一种特别形式,借助资源共享的特点,可以充分利用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 当多余核心线程数的线程空闲多久时间时会释放掉。
使用时需要关注以下几点:
-
方法直接调用即可,此时需要在执行方法的主程序中增加类注解@EnableAysnc;
-
交由线程池管理方法和执行该任务的方法不可以在同一个类中;
-
多个方法可以共享一个线程池,@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;
}
本文主要是对前段时间学习和工作实践遇到知识点的学习和积累,过程中参考了技术博客与书籍,欢迎阅读的各位批评指正。