在java中异步线程很重要,比如在业务流处理时,需要通知硬件设备,发短信通知用户,或者需要上传一些图片资源到其他服务器这种耗时的操作,在主线程里处理会阻塞整理流程,而且我们也不需要等待处理结果之后再进行下一步操作,这时候就可以使用异步线程进行处理,这样主线程不会因为这些耗时的操作而阻塞,保证主线程的流程可以正常进行。
Spring
用TaskExecutor
和TaskScheduler
接口提供了异步执行
和调度任务
的抽象.
Spring
的TaskExecutor
和java.util.concurrent.Executor
接口时一样的,这个接口只有一个方法execute(Runnable task)
。
Spring已经内置了许多TaskExecutor的实现,没有必要自己去实现:
SimpleAsyncTaskExecutor:不是真的线程池,这个类不重用线程,每次调用都会创建一个新的线程。
SyncTaskExecutor:这个类没有实现异步调用,只是一个同步操作。只适用于不需要多线程的地方
ConcurrentTaskExecutor:Executor的适配类,不推荐使用。如果ThreadPoolTaskExecutor不满足要求时,才用考虑使用这个类
SimpleThreadPoolTaskExecutor:是Quartz的SimpleThreadPool的类。线程池同时被quartz和非quartz使用,才需要使用此类
ThreadPoolTaskExecutor :最常使用,推荐。 其实质是对java.util.concurrent.ThreadPoolExecutor的包装
SimpleAsyncTaskExecutor:不是真的线程池, 这种实现不会重用任何线程,每次调用都会创建一个新的线程。
SyncTaskExecutor: 这种实现不会异步的执行,相反,每次调用都在发起调用的线程中执行。它的主要用处是在不需要多线程的时候,比如简单的测试用例;
ConcurrentTaskExecutor:这个实现是对Java 5 java.util.concurrent.Executor类的包装。如果ThreadPoolTaskExecutor不满足要求时,才用考虑使用这个类
SimpleThreadPoolTaskExecutor: 这个实现实际上是Quartz的SimpleThreadPool类的子类,它会监听Spring的生命周期回调。当有线程池,需要在Quartz和非Quartz组件中共用时,这是它的典型用处。
ThreadPoolTaskExecutor:这是最常用、最通用的一种实现。它包含了java.util.concurrent.ThreadPoolExecutor的属性,( 其实质是对java.util.concurrent.ThreadPoolExecutor的包装).并且用TaskExecutor进行包装。
1.2 异步说明和原理
使用地方说明:
在方法上使用该@Async注解,申明该方法是一个异步任务;
在类上面使用该@Async注解,申明该类中的所有方法都是异步任务;
使用此注解的方法的类对象,必须是spring管理下的bean对象;
要想使用异步任务,需要在主类上开启异步配置,即,配置上@EnableAsync注解;
@Async的原理概括:
@Async的原理是通过 Spring AOP 动态代理 的方式来实现的。
Spring 容器启动初始化bean时,判断类中是否使用了@Async注解,如果使用了则为其创建切入点和切入点处理器,根据切入点创建代理,在线程调用@Async注解标注的方法时,会调用代理,执行切入点处理器invoke方法,将方法的执行提交给线程池中的另外一个线程来处理,从而实现了异步执行。
所以,需要注意的一个错误用法是,如果a方法调用它同类中的标注@Async的b方法,是不会异步执行的,因为从a方法进入调用的都是该类对象本身,不会进入代理类。
因此,相同类中的方法调用带@Async的方法是无法异步的,这种情况仍然是同步
@Async使用
在Spring中启用@Async:
@Async注解在使用时,如果不指定线程池的名称,则使用Spring默认的线程池,Spring默认的线程池为SimpleAsyncTaskExecutor。(这种实现不会重用任何线程,每次调用都会创建一个新的线程)
方法上一旦标记了这个@Async注解,当其它线程调用这个方法时,就会开启一个新的子线程去异步处理该业务逻辑。Spring
默认的线程池的默认配置:
默认核心线程数:8,
最大线程数:Integet.MAX_VALUE,
队列使用LinkedBlockingQueue,
容量是:Integet.MAX_VALUE,
空闲线程保留时间:60s,
线程池拒绝策略:AbortPolicy()
缺点
:从最大线程数的配置上,相信看到问题:并发情况下,会无限创建线程
默认线程池的上述缺陷如何解决:答案是,自定义配置参数就可以了
在配置文件中配置
spring:
task:
execution:
pool:
max-size: 6
core-size: 3
keep-alive: 3s
queue-capacity: 1000
thread-name-prefix: name
自定义线程池
在业务场景中,有时需要使用自己定义的执行器来跑异步的业务逻辑,那该怎么办呢?答案是,自定义线程池。
编写配置类
ThreadPoolExecutor是Java的线程池
ThreadPoolTaskExecutor是spring封装的线程池
package com.thgy.bc.common.config;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.*;
@Slf4j
@Configuration
public class ThreadPoolConfig {
@Bean("task")
public ThreadPoolTaskExecutor threadPoolTaskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
int i = Runtime.getRuntime().availableProcessors();
//核心线程数目
executor.setCorePoolSize(i * 2);
//指定最大线程数
executor.setMaxPoolSize(i * 2);
//队列中最大的数目
executor.setQueueCapacity(i * 2 * 10);
//线程名称前缀
executor.setThreadNamePrefix("ThreadPoolTaskExecutor-");
//rejection-policy:当pool已经达到max size的时候,如何处理新任务
//CALLER_RUNS:不在新线程中执行任务,而是由调用者所在的线程来执行
//对拒绝task的处理策略
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
//当调度器shutdown被调用时等待当前被调度的任务完成
executor.setWaitForTasksToCompleteOnShutdown(true);
//线程空闲后的最大存活时间
executor.setKeepAliveSeconds(60);
//加载
executor.initialize();
log.info("初始化线程池成功");
return executor;
}
@Bean
public ThreadPoolExecutor threadPoolExecutor() {
//获取cpu核心数
int i = Runtime.getRuntime().availableProcessors();
//核心线程数
int corePoolSize = i * 2;
//最大线程数
int maximumPoolSize = i * 2;
//线程无引用存活时间
long keepAliveTime = 60;
//时间单位
TimeUnit unit = TimeUnit.SECONDS;
//任务队列,接收一个整型的参数,这个整型参数指的是队列的长度,
//ArrayBlockingQueue(int,boolean),boolean类型的参数是作为可重入锁的参数进行初始化,默认false,另外初始化了notEmpty、notFull两个信号量。
BlockingQueue<Runnable> workQueue = new ArrayBlockingQueue(i * 2 * 10);
//1. 同步阻塞队列 (put,take),直接提交。直接提交策略表示线程池不对任务进行缓存。新进任务直接提交给线程池,当线程池中没有空闲线程时,创建一个新的线程处理此任务。
// 这种策略需要线程池具有无限增长的可能性。实现为:SynchronousQueue
//2. 有界队列。当线程池中线程达到corePoolSize时,新进任务被放在队列里排队等待处理。有界队列(如ArrayBlockingQueue)有助于防止资源耗尽,
// 但是可能较难调整和控制。队列大小和最大池大小可能需要相互折衷:使用大型队列和小型池可以最大限度地降低 CPU 使用率、操作系统资源和上下文切换开销,
// 但是可能导致人工降低吞吐量。如果任务频繁阻塞(例如,如果它们是 I/O 边界),则系统可能为超过您许可的更多线程安排时间。使用小型队列通常要求较大的池大小,
// CPU 使用率较高,但是可能遇到不可接受的调度开销,这样也会降低吞吐量。
//3. 无界队列。使用无界队列(例如,不具有预定义容量的 LinkedBlockingQueue)将导致在所有 corePoolSize 线程都忙时新任务在队列中等待。
// 这样,创建的线程就不会超过 corePoolSize。(因此,maximumPoolSize 的值也就无效了。)当每个任务完全独立于其他任务,即任务执行互不影响时,
// 适合于使用无界队列;例如,在 Web 页服务器中。这种排队可用于处理瞬态突发请求,当命令以超过队列所能处理的平均数连续到达时,此策略允许无界线程具有增长的可能性。
//线程工厂
//defaultThreadFactory()
//返回用于创建新线程的默认线程工厂。
//privilegedThreadFactory()
//返回一个用于创建与当前线程具有相同权限的新线程的线程工厂。
ThreadFactory threadFactory =Executors.defaultThreadFactory();
//拒绝执行处理器
RejectedExecutionHandler handler = new ThreadPoolExecutor.CallerRunsPolicy();
//创建线程池
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory, handler);
return threadPoolExecutor;
}
}
使用自定义线程池
在方法上的@Async
注解处指定线程池名字:
@Component
public class MyAsyncTask {
@Async("task") //使用自定义的线程池(执行器)
public void asyncCpsItemImportTask(Long platformId, String jsonList){
//...具体业务逻辑
}
}
线程池拒绝策略
RejectedExecutionHandler
handler 线程池的拒绝策略,是指当任务添加到线程池中被拒绝,而采取的处理措施。
当任务添加到线程池中之所以被拒绝,可能是由于:第一,线程池异常关闭。第二,任务数量超过线程池的最大限制。Reject
策略预定义有四种:
ThreadPoolExecutor.CallerRunsPolicy 是一种可伸缩队列策略(自己项目用的策略)。这种策略是当线程池中的线程数达到最大值,并且已提交的任务数大于线程池中的线程数时,任务将在调用者线程中执行,而不是在线程池中的线程中执行。这个策略可以控制任务的执行速度,防止线程池中的任务过多导致的资源耗尽。
1.ThreadPoolExecutor.AbortPolicy策略,是默认的策略,处理程序遭到拒绝将抛出运行时 RejectedExecutionException
2.ThreadPoolExecutor.CallerRunsPolicy策略 ,调用者的线程会执行该任务,如果执行器已关闭,则丢弃.
3.ThreadPoolExecutor.DiscardPolicy策略,不能执行的任务将被丢弃.
4.ThreadPoolExecutor.DiscardOldestPolicy策略,如果执行程序尚未关闭,则位于工作队列头部的任务将被删除,然后重试执行程序(如
果再次失败,则重复此过程)
自定义策略:当然也可以根据应用场景需要来实现RejectedExecutionHandler接口自定义策略。如记录日志或持久化不能处理的任务
7.jdk自带的四种线程池创建方式(一共有七种创建线程池的方式)
// 第一种线程池:固定个数的线程池,可以为每个CPU核绑定一定数量的线程数
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(processors * 2);
// 缓存线程池,无上限
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
// 单一线程池,永远会维护存在一条线程
ExecutorService singleThreadPool = Executors.newSingleThreadExecutor();
// 固定个数的线程池,可以执行延时任务,也可以执行带有返回值的任务。
ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(5);
第一个线程池提供的功能:
1. 设置(线程池中)线程的命名规则。
2. 设置线程的优先级。
3. 设置线程分组。
4. 设置线程类型(用户线程、守护线程)。
第二个线程池:
优点:线程池会根据任务数量创建线程池,并且在一定时间内可以重复使用这些线程,产生相应的线程池。
缺点:适用于短时间有大量任务的场景,它的缺点是可能会占用很多的资源。
第三个单线程线程池:
单线程的线程池又什么意义?
1. 复用线程。
2. 单线程的线程池提供了任务队列和拒绝策略(当任务队列满了之后(Integer.MAX_VALUE),新来的任务就会拒绝策略)
线程池的创建
线程池的创建⽅法总共有 7 种,但总体来说可分为 2 类
1. 通过 ThreadPoolExecutor 创建的线程池;
2. 通过 Executors 创建的线程池。
线程池的创建⽅式总共包含以下 7 种(其中 6 种是通过 Executors 创建的,1 种是通过ThreadPoolExecutor 创建的):
1. Executors.newFixedThreadPool:创建⼀个固定⼤⼩的线程池,可控制并发的线程数,超出的线程会在队列中等待;
2. Executors.newCachedThreadPool:创建⼀个可缓存的线程池,若线程数超过处理所需,缓存⼀段时间后会回收,若线程数不够,则新建线程;
3. Executors.newSingleThreadExecutor:创建单个线程数的线程池,它可以保证先进先出的执⾏顺序;
4. Executors.newScheduledThreadPool:创建⼀个可以执⾏延迟任务的线程池;
5. Executors.newSingleThreadScheduledExecutor:创建⼀个单线程的可以执⾏延迟任务的线程池;
6. Executors.newWorkStealingPool:创建⼀个抢占式执⾏的线程池(任务执⾏顺序不确定)【JDK1.8 添加】。
7. ThreadPoolExecutor:最原始的创建线程池的⽅式,它包含了 7 个参数可供设置,后⾯会详细讲。
4.线程池的执行顺序 线程池按以下行为执行任务
- 当线程数小于核心线程数时,创建线程。
- 当线程数大于等于核心线程数,且任务队列未满时,将任务放入任务队列。
- 当线程数大于等于核心线程数,且任务队列已满,若线程数小于最大线程数,创建线程。
- 若线程数等于最大线程数,则执行拒绝策略
3.线程池的核心参数
public ThreadPoolExecutor(int corePoolSize,//核心线程数
int maximumPoolSize,//最大线程数
long keepAliveTime,//线程空闲时间
TimeUnit unit,//时间单位
BlockingQueue<Runnable> workQueue,//任务队列
ThreadFactory threadFactory,//线程工厂
RejectedExecutionHandler handler//拒绝策略)
{
...
}
5.线程池的参数详解
- corePoolSize
核心线程数,默认为1。
设置规则:
CPU密集型(CPU密集型也叫计算密集型,指的是运算较多,cpu占用高,读/写I/O(硬盘/内存)较少):corePoolSize = CPU核数 + 1
IO密集型(与cpu密集型相反,系统运作,大部分的状况是CPU在等I/O (硬盘/内存) 的读/写操作,此时CPU Loading并不高。):corePoolSize = CPU核数 * 2
- maximumPoolSize
最大线程数,默认为Integer.MAX_VALUE
一般设置为和核心线程数一样
- keepAliveTime
线程空闲时间,默认为60s,一般设置为默认60s
- unit
时间单位,默认为秒
- workQueue
队列,当线程数目超过核心线程数时用于保存任务的队列。(BlockingQueue workQueue)此队列仅保存实现Runnable接口的任务。(因为线程池的底层BlockingQueue的泛型为Runnable)
无界队列
队列大小无限制,常用的为无界的LinkedBlockingQueue,使用该队列作为阻塞队列时要尤其当心,当任务耗时较长时可能会导致大量新任务在队列中堆积最终导致OOM。阅读代码发现,Executors.newFixedThreadPool 采用就是 LinkedBlockingQueue,而博主踩到的就是这个坑,当QPS很高,发送数据很大,大量的任务被添加到这个无界LinkedBlockingQueue 中,导致cpu和内存飙升服务器挂掉。
当然这种队列,maximumPoolSize 的值也就无效了。当每个任务完全独立于其他任务,即任务执行互不影响时,适合于使用无界队列;例如,在 Web 页服务器中。这种排队可用于处理瞬态突发请求,当命令以超过队列所能处理的平均数连续到达时,此策略允许无界线程具有增长的可能性。
有界队列
当使用有限的 maximumPoolSizes 时,有界队列有助于防止资源耗尽,但是可能较难调整和控制。常用的有两类,一类是遵循FIFO原则的队列如ArrayBlockingQueue,另一类是优先级队列如PriorityBlockingQueue。PriorityBlockingQueue中的优先级由任务的Comparator决定。
使用有界队列时队列大小需和线程池大小互相配合,线程池较小有界队列较大时可减少内存消耗,降低cpu使用率和上下文切换,但是可能会限制系统吞吐量。
同步移交队列
如果不希望任务在队列中等待而是希望将任务直接移交给工作线程,可使用SynchronousQueue作为等待队列。SynchronousQueue不是一个真正的队列,而是一种线程之间移交的机制。要将一个元素放入SynchronousQueue中,必须有另一个线程正在等待接收这个元素。只有在使用无界线程池或者有饱和策略时才建议使用该队列。
原文:线程池中各个参数如何合理设置_线程池配置参数数值如何确定_老周聊架构的博客-CSDN博客
- threadFactory
线程工厂,用来创建线程。
为了统一在创建线程时设置一些参数,如是否守护线程,线程一些特性等,如优先级。通过这个TreadFactory创建出来的线程能保证有相同的特性。
它是一个接口类,而且方法只有一个,就是创建一个线程。
如果没有另外说明,则在同一个ThreadGroup 中一律使用Executors.defaultThreadFactory() 创建线程,并且这些线程具有相同的NORM_PRIORITY 优先级和非守护进程状态。
通过提供不同的 ThreadFactory,可以改变线程的名称、线程组、优先级、守护进程状态,等等。
如果从newThread 返回 null 时ThreadFactory 未能创建线程,则执行程序将继续运行,但不能执行任何任务。
- handler
拒绝策略,默认是AbortPolicy,会抛出异常。
当线程数已经达到maxPoolSize,且队列已满,会拒绝新任务。
当线程池被调用shutdown()后,会等待线程池里的任务执行完毕再shutdown。如果在调用shutdown()和线程池真正shutdown之间提交任务,会拒绝新任务。
AbortPolicy 丢弃任务,抛运行时异常。
CallerRunsPolicy 由当前调用的任务线程执行任务。
DiscardPolicy 忽视,什么都不会发生。
DiscardOldestPolicy 从队列中踢出最先进入队列(最后一个执行)的任务。
调用线程池的线程案例
public List<AccountRecordVO> requestTest() throws ExecutionException, InterruptedException {
List<String> ids = Lists.newArrayList();
ids.add("1");
ids.add("2");
ids.add("3");
ids.add("4");
//有返回值的情况,定义接收返回值
List<AccountRecordVO> futureList2 = Lists.newArrayList();
//分布式计数器,若业务不需要则可以不定义
CountDownLatch countDownLatch = new CountDownLatch(ids.size());
for (String id : ids) {
//调用线程池的线程执行任务
threadPoolTaskExecutor.submit(new Runnable() {
@Override
public void run() {
test(Lists.newArrayList(id),futureList2);
//计数器-1
countDownLatch.countDown();
}
});
}
//await阻塞,直到计数器为0
countDownLatch.await();
System.out.println("主线程"+"====");
return futureList2;
}
public List<AccountRecordVO> test(List<String> ids, List<AccountRecordVO> list2){
//随便写的业务逻辑代码,无实际意义,仅作演示
System.out.println("线程体" + "====");
List<AccountRecordVO> accountRecordVOS = Lists.newArrayList();
int i = 0;
AccountRecordVO accountRecordVO = new AccountRecordVO();
accountRecordVO.setUserId("123");
accountRecordVO.setAmount(12333);
for (String id : ids){
accountRecordVOS.add(accountRecordVO);
list2.add(accountRecordVO);
}
try{
Thread.sleep(Long.valueOf("1000"));
}catch (Exception e){
log.error(e.getMessage());
}
System.out.println("线程体结束" + "====");
return accountRecordVOS;
}
threadPoolTaskExecutor.submit可以有返回值
异步返回
异步的业务逻辑处理场景 有两种:一个是不需要返回结果,另一种是需要接收返回结果。不需要返回结果的比较简单,就不多说了。
需要接收返回结果的示例如下:
@Async("task")
public Future<Map<Long, List>> queryMap(List ids) {
List<> result = businessService.queryMap(ids);
..............
Map<Long, List> resultMap = Maps.newHashMap();
...
return new AsyncResult<>(resultMap);
}
调用异步方法的示例:
future.isDone() //可以判断线程是否执行完毕,下面没用到,可以用
public Map<Long, List> asyncProcess(List<BindDeviceDO> bindDevices,List<BindStaffDO> bindStaffs, String dccId) {
Map<Long, List> finalMap =null;
// 返回值:
Future<Map<Long, List>> asyncResult = MyService.queryMap(ids);
try {
finalMap = asyncResult.get();
} catch (Exception e) {
...
}
return finalMap;
}
为什么使用线程池
- 降低系统资源消耗,通过重用已存在的线程,降低线程创建和销毁造成的消耗;
线程池为什么需要使用队列
因为线程若是无限制的创建,可能会导致内存
占用过多而产生OOM
,并且会造成cpu
过度切换。
创建线程池的消耗较高或者线程池创建线程需要获取mainlock
这个全局锁,影响并发效率,阻塞队列可以很好的缓冲
线程池为什么要使用阻塞队列而不使用非阻塞队列
阻塞队列可以保证任务队列中没有任务时阻塞获取任务的线程,使得线程进入wait状态,释放cpu资源,当队列中有任务时才唤醒对应线程从队列中取出消息进行执行。
使得在线程不至于一直占用cpu资源。(线程执行完任务后通过循环再次从任务队列中取出任务进行执行,代码片段如:while (task != null || (task = getTask()) != null) {})。
如何配置线程池(cpu核心数加1)
CPU密集型任务
尽量使用较小的线程池,一般为CPU核心数+1。 因为CPU密集型任务使得CPU使用率很高,若开过多的线程数,会造成CPU过度切换
IO密集型任务
可以使用稍大的线程池,一般为2*CPU核心数。 IO密集型任务CPU使用率并不高,因此可以让CPU在等待IO的时候有其他线程去处理别的任务,充分利用CPU时间
混合型任务
可以将任务分成IO密集型和CPU密集型任务,然后分别用不同的线程池去处理。 只要分完之后两个任务的执行时间相差不大,那么就会比串行执行来的高效
因为如果划分之后两个任务执行时间有数据级的差距,那么拆分没有意义。
因为先执行完的任务就要等后执行完的任务,最终的时间仍然取决于后执行完的任务,而且还要加上任务拆分与合并的开销,得不偿失
execute()和submit()方法
execute(),执行一个任务,没有返回值
submit(),提交一个线程任务,有返回值
submit(Callable<T> task)能获取到它的返回值,通过future.get()获取(阻塞直到任务执行完)。一般使用FutureTask+Callable配合使用
submit(Runnable task, T result)能通过传入的载体result间接获得线程的返回值。
submit(Runnable task)则是没有返回值的,就算获取它的返回值也是null
Future.get()方法会使取结果的线程进入阻塞状态,直到线程执行完成之后,唤醒取结果的线程,然后返回结果
execute和submit的区别
execute和submit都属于线程池的方法,execute只能提交Runnable类型的任务
submit既能提交Runnable类型任务也能提交Callable类型任务。
execute()没有返回值
submit有返回值,所以需要返回值的时候必须使用submit文章来源:https://www.toymoban.com/news/detail-739556.html
使用submit可以执行有返回值的任务或者是无返回值的任务;而execute只能执行不带返回值的任务。
文章来源地址https://www.toymoban.com/news/detail-739556.html
到了这里,关于Spring (Async)中的线程池的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!