目录
需求背景
在开发需求的时候经常会有需要多线程同时协同执行一个任务的情况。这时候会有很多种解决方案。这里针对不同的场景和需求,给一个多线程执行任务的一个方案参考
可能会有的需求点
- 多线程分发一份数据然后以相同的逻辑去执行
- 需要知道什么时候执行完了
- 拥有较好的性能,方便的参数调整
方案实现
对于java来说,一般我们都会使用spring或者springboot来作为框架,在spring框架里面有个@Async注解,如果这个注解标注在方法上,当其他bean调用这个方法的时候,就会用线程池去异步执行这个方法。这是基于代理来实现的,一般方法内调用,如果不使用特殊调用方式,是无法代理的,要注意。还有一种方法就是自己去实现一个线程池来作为多线程执行任务的线程池,相比于@Async注解来说,自己实现可以掌控的细节更多,更加灵活,缺点是比较容易忽略一些问题导致bug。本文这里主要就是讲述这个线程池如何写又方便又能满足大部分场景的需求。
总体而言,可以分为以下两个方法
- 自己写线程池来实现
- 使用@Async注解来实现
使用Async注解来实现多线程执行任务
适用的需求范围
- [✔]多线程分发一份数据然后以相同的逻辑去执行
- [✖]知道什么时候执行完了
- [✔]拥有较好的性能,方便的参数调整
实现方式
用注解来实现异步多线程调用的方式网上教程很多,这里就挑重点说说。
- 第一步:在启动类加上@EnableAsync注解开启异步支持,这里启动类就是springboot的启动类
- 第二步:创建一个Async对应的一个config配置类,代码示例如下
@Bean("doSomething")
public Executor getKeywordsExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
// 核心线程数:线程池创建时候初始化的线程数
executor.setCorePoolSize(2);
// 最大线程数:线程池最大的线程数,只有在缓冲队列满了之后才会申请超过核心线程数的线程
executor.setMaxPoolSize(2);
// 缓冲队列:用来缓冲执行任务的队列
executor.setQueueCapacity(1000);
// 允许线程的空闲时间60秒:当超过了核心线程之外的线程在空闲时间到达之后会被销毁
executor.setKeepAliveSeconds(60);
// 线程池名的前缀:设置好了之后可以方便我们定位处理任务所在的线程池
executor.setThreadNamePrefix("do-something-");
// 缓冲队列满了之后的拒绝策略:由调用线程处理(一般是主线程)
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
executor.initialize();
return executor;
}
- 第三步,可以看到这里的bean的名字是“doSomething”,而后使用@Async("doSomething")来注解某一个bean里面的void方法,这样一来,这个bean的这个方法就会被代理,当通过这个bean去执行这个方法的时候,就会被代理,而后通过配置的线程池来执行这个方法
主要就是这三部,非常简单。
手动实现线程池多线程执行任务
适用的需求范围
- [✔]多线程分发一份数据然后以相同的逻辑去执行
- [✔]]知道什么时候执行完了
- [✔]拥有较好的性能,方便的参数调整
实现方式
最核心的设计以及和Async不一样的地方是这里为了知道线程池里面的任务什么时候执行完,使用了线程池的shutdown()和awaitTermination()方法来确保里面所有任务执行完了。为了方便调用,这里写个基础类来实现逻辑,调用方法为继承这个基础类然后重写具体的实现逻辑
具体代码如下
public abstract class BaseTaskService{
public NamedThreadPoolExecutor dealPool;
public void doTaskWithPool() {
this.dealPool =
new NamedThreadPoolExecutor(
32,
128,
60,
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(5),
new ThreadPoolExecutor.CallerRunsPolicy(),
this.getClass().getSimpleName());
}
public void doTaskWithPool(NamedThreadPoolExecutor dealPool) {
this.dealPool = dealPool;
}
public abstract void doWithPool(List<JSONObject> datas, long time);
public void starWithPool(List<JSONObject> datas, long time) {
CommonRunnable commonRunnable = new CommonRunnable(this, datas, time);
this.dealPool.execute(commonRunnable);
}
public void waitForEnd() {
try {
dealPool.shutdown();
this.dealPool.awaitTermination(Long.MAX_VALUE, TimeUnit.NANOSECONDS);
this.dealPool = null;
} catch (Exception e) {
log.error("dealPool.awaitTermination error", e);
}
}
}
其中doTaskWithPool为初始化线程池方法,有两种入参,一种是无参,使用默认的线程池配置,一种是自己创建一个线程池进进阿里,每次开启任务的时候需要调用,而执行完任务调用waitForEnd会自动销毁,并且会等待所有的任务执行完毕。
其中doWithPool是一个抽象方法,即为子类需要实现的方法,这个方法用于执行任务,入参JSONObject可以根据实际需要去修改。
这里附上这个基础类用到的其他代码
public class NamedThreadPoolExecutor extends ThreadPoolExecutor {
private final String poolName;
public NamedThreadPoolExecutor(
int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
RejectedExecutionHandler rejectedExecutionHandler,
String poolName) {
super(
corePoolSize,
maximumPoolSize,
keepAliveTime,
unit,
workQueue,
new NamedThreadFactory(poolName),
rejectedExecutionHandler);
this.poolName = poolName;
}
public String getPoolName() {
return poolName;
}
private static class NamedThreadFactory implements ThreadFactory {
private final String poolName;
private final AtomicInteger threadNumber = new AtomicInteger(1);
NamedThreadFactory(String poolName) {
this.poolName = poolName;
}
public Thread newThread(Runnable r) {
return new Thread(r, poolName + "-thread-" + threadNumber.getAndIncrement());
}
}
}
这个是一个可以指定名字的线程池,方便日志打印线程名字
@AllArgsConstructor
@NoArgsConstructor
@Data
public class CommonRunnable implements Runnable {
private BaseTaskService baseTaskService;
private List<JSONObject> datas;
private long time;
/**
* When an object implementing interface {@code Runnable} is used to create a thread, starting
* the thread causes the object's {@code run} method to be called in that separately executing
* thread.
*
* <p>The general contract of the method {@code run} is that it may take any action whatsoever.
*
* @see Thread#run()
*/
@Override
public void run() {
baseTaskService.doWithPool(datas, time);
}
}
runnable,没什么好说的,用来当载体
使用方法
完成base类的代码编写后,如何调用呢这里用一个示例来介绍
public class MusicFixService extends BaseTaskService {
public void doTask() {
doTaskWithPool();
while (true) {
List<JSONObject> data = getData()
starWithPool(data, time);
}
waitForEnd();
// 全部执行完了
}
@Override
public void doWithPool(List<JSONObject> datas, long time) {
//这个方法里面写任务的具体逻辑
}
}
如果有什么疑问可以留言,我都会去看的