环境:
- JDK8
- SpringBoot 2.7.2
- Lombok 1.18.20
- HuTool 5.8.10
- guava 27.1-jre
本文章是对Java线程池实现的一些基本方案,主要实现了以下几个功能:
- 自定义拒绝策略
- 任务重试机制(可配置具体细节)
- 快慢线程池(分别处理快慢任务)
- 动态调整线程池参数(只包含核心属性)
- 手动执行任务(针对单节点服务中定时任务)
线程池动态实现参考美团文章:Java线程池实践
本文章全部源码(觉得有用的话点个star吧!):https://github.com/demsiadh/ScheduleUtil
本文章不含测试内容,所有内容均已测试通过,感兴趣可以拉下来测一把
一、原生线程池的局限性
在程序中使用线程池,主要是为了多线程执行任务下对线程进行管理,同时减少了上下文切换提高了处理效率,但是目前原生线程池的实现方案并不可靠,包括拒绝策略,在项目中都是无法直接使用的
无法定义的线程池参数
原生线程池,在定义的时候就已经写死了核心线程数和最大线程数等属性,但是我们并没有一个公式或者可靠的方案来定义,虽然我们都知道IO密集型(2n)、计算密集型(n+1),但是在实际项目中错综复杂,无法定义,参数设置不当不仅享受不到线程池的优势,甚至还有可能导致故障。
不可靠的拒绝策略
在Java线程池中,拒绝策略在线程池无法处理新任务的时候触发(最大线程数和阻塞队列都已经满了),默认的拒绝策略有四个,他们的作用以及局限性如下:
- AbortPolicy(默认策略)
- 作用:直接抛出异常
- 缺点:在项目中我们不希望任务被抛弃或者抛出异常
- CallerRunsPolicy
- 作用:让调用者线程来执行任务
- 缺点:如果有大量任务都触发这个拒绝策略,对服务器的压力还是挺大的,甚至可能导致无法处理其他任务
- DiscardOldestPolicy
- 作用:丢弃最早未处理的任务,让当前任务入队
- 缺点:在一个可靠的项目中,我们不希望任何一个任务被抛弃
- DiscardPolicy
- 作用:直接丢弃当前任务
- 缺点:在一个可靠的项目中,我们不希望任何一个任务被抛弃
二、可靠线程池的实现
我们需要实现一个可靠线程池,就必须保证没有任务会被抛弃,并且能够处理各种任务
1.自定义实现
自定义拒绝策略
线程池的拒绝策略都实现了一个接口RejectedExecutionHandler,我们定义一个ScheduleRejected来实现这个接口,具体策略我们采用简单的队列的put方法(会使当前线程进入阻塞);
@Component
@Slf4j
public class ScheduleRejected implements RejectedExecutionHandler {
/**
* 当线程池满的时候,直接调用队列的put方法来进行阻塞
* @param r the runnable task requested to be executed
* @param executor the executor attempting to execute this task
*/
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
try {
executor.getQueue().put(r);
} catch (InterruptedException e) {
// 不会被打断
}
}
}
自定义阻塞队列
原生的阻塞队列由于其容量(capacity)都被final修饰,不可被修改,所以我们需要自行实现一个阻塞队列,为其添加get,set方法,为了保证线程安全需要添加volatile,实现方式也很简单,直接赋值jdk源码中的LinkedBlockingQueue的全部代码并修改名称ResizeLinkedBlockingQueue,并修改部分方法中的判断即可;
- capacity属性
@Getter
private volatile int capacity;
public void setCapacity(int capacity) {
if (capacity <= 0)
throw new IllegalArgumentException();
this.capacity = capacity;
}
- 构造函数
public ResizeLinkedBlockingQueue(Collection<? extends E> c) {
this(Integer.MAX_VALUE);
final ReentrantLock putLock = this.putLock;
putLock.lock(); // Never contended, but necessary for visibility
try {
int n = 0;
for (E e : c) {
if (e == null)
throw new NullPointerException();
// 在这里
if (n >= capacity)
throw new IllegalStateException("Queue full");
enqueue(new Node<E>(e));
++n;
}
count.set(n);
} finally {
putLock.unlock();
}
}
- put方法
public void put(E e) throws InterruptedException {
if (e == null) throw new NullPointerException();
// Note: convention in all put/take/etc is to preset local var
// holding count negative to indicate failure unless set.
int c = -1;
Node<E> node = new Node<E>(e);
final ReentrantLock putLock = this.putLock;
final AtomicInteger count = this.count;
putLock.lockInterruptibly();
try {
/*
* Note that count is used in wait guard even though it is
* not protected by lock. This works because count can
* only decrease at this point (all other puts are shut
* out by lock), and we (or some other waiting put) are
* signalled if it ever changes from capacity. Similarly
* for all other uses of count in other wait guards.
*/
// 这里
while (count.get() >= capacity) {
notFull.await();
}
enqueue(node);
c = count.getAndIncrement();
if (c + 1 < capacity)
notFull.signal();
} finally {
putLock.unlock();
}
if (c == 0)
signalNotEmpty();
}
4.offer方法
public boolean offer(E e, long timeout, TimeUnit unit)
throws InterruptedException {
if (e == null) throw new NullPointerException();
long nanos = unit.toNanos(timeout);
int c = -1;
final ReentrantLock putLock = this.putLock;
final AtomicInteger count = this.count;
putLock.lockInterruptibly();
try {
// 在这里
while (count.get() >= capacity) {
if (nanos <= 0)
return false;
nanos = notFull.awaitNanos(nanos);
}
enqueue(new Node<E>(e));
c = count.getAndIncrement();
if (c + 1 < capacity)
notFull.signal();
} finally {
putLock.unlock();
}
if (c == 0)
signalNotEmpty();
return true;
}
public boolean offer(E e) {
if (e == null) throw new NullPointerException();
final AtomicInteger count = this.count;
// 在这里
if (count.get() >= capacity)
return false;
int c = -1;
Node<E> node = new Node<E>(e);
final ReentrantLock putLock = this.putLock;
putLock.lock();
try {
if (count.get() < capacity) {
enqueue(node);
c = count.getAndIncrement();
if (c + 1 < capacity)
notFull.signal();
}
} finally {
putLock.unlock();
}
if (c == 0)
signalNotEmpty();
return c >= 0;
}
- clear方法
public void clear() {
fullyLock();
try {
for (Node<E> p, h = head; (p = h.next) != null; h = p) {
h.next = h;
p.item = null;
}
head = last;
// assert head.item == null && head.next == null;
// 在这里
if (count.getAndSet(0) >= capacity)
notFull.signal();
} finally {
fullyUnlock();
}
}
2.线程池工具
可重试任务
- 判定是否成功:既然需要可以重试,所以我们需要传入的任务可以获取到运行结果,所以就采用Java的函数式编程来实现,这里我们定义一个消费者来消费传入的任务(taskId,是用于标识任务的唯一标志,使用时建议采用类名.方法名)
// 消费者接口
private interface DConsumer<T, R> {
void accept(T t, R r);
}
// 消费者
private final DConsumer<String, ScheduledRunnable> cff = (taskId, scheduledRunnable) -> {
LocalDateTime startTime = LocalDateTime.now();
log.info("{} task start", taskId);
try {
// 接受任务执行的返回值
Boolean apply = scheduledRunnable.supplier.get();
// 如果执行失败就重试
if (!apply) {
addTask(scheduledRunnable);
}
} catch (Exception e) {
log.error("{} task fail Exception: {}", taskId, e.getMessage(), e);
}finally {
log.info("{} task finish!", taskId);
// 记录当前任务执行时间
TASK_TIME_MAP.put(taskId, LocalDateTimeUtil.between(startTime, LocalDateTime.now()).getSeconds());
}
};
- 内部记录次数和时间:既然是重试,肯定要有个重试次数和重试时间,由于定时调度采用的是ScheduledExecutorService,所以我们创建一个内部类实现Runnable,同时内部记录相关信息
public class ScheduledRunnable implements Runnable{
private int retryTimes;
private final String taskId;
private final Supplier<Boolean> supplier;
private final LocalDateTime deadlineTime;
public ScheduledRunnable(String taskId, Supplier<Boolean> supplier, LocalDateTime deadlineTime) {
this.taskId = taskId;
this.supplier = supplier;
this.deadlineTime = deadlineTime;
retryTimes = 0;
}
@Override
public void run() {
// 如果重试次数超过最大次数或者超过最大重试时间则移除任务
if (retryTimes++ >= scheduleConfig.getMaxRetryTimes()) {
remove(taskId);
log.error("{} task retry times is over max retry times!", taskId);
return;
} else if (LocalDateTime.now().isAfter(deadlineTime)) {
remove(taskId);
log.error("{} task retry times is over deadline time!", taskId);
return;
}
log.info("{} task retry", taskId);
// 执行任务 如果执行成功则移除任务
boolean retryResult = supplier.get();
if (retryResult) {
remove(taskId);
}
}
}
- 添加任务到定时调度线程池中(FUTURE_MAP记录了所有在定时任务调度线程池中的任务)
private static ScheduledExecutorService service;
// 任务集合
private static final Map<String, ScheduledFuture<?>> FUTURE_MAP = new ConcurrentHashMap<>();
@Resource
private ScheduleConfig scheduleConfig;
@PostConstruct
public void init() {
// 初始化线程工厂 设置为守护线程
ThreadFactory threadFactory = new ThreadFactoryBuilder()
.setNameFormat("schedule-pool-%d").setDaemon(true).build();
// 如果定时线程池已经存在则先关闭
if (service != null) {
service.shutdown();
}
// 创建定时任务线程池 异常处理直接抛出异常
service = new ScheduledThreadPoolExecutor(2, threadFactory, new ThreadPoolExecutor.AbortPolicy());
}
/**
* 添加定时任务(一定时间后重新执行)
*
* @param retryTask 需要重试的任务对象
*/
private void addTask(ScheduledRunnable retryTask) {
// 如果重试任务已经在map中则直接返回
if (FUTURE_MAP.get(retryTask.taskId) != null) {
log.error("ScheduledService add task Failed! {} task already exist", retryTask.taskId);
return;
}
// 将任务添加到定时服务中
ScheduledFuture<?> scheduledFuture = service.scheduleWithFixedDelay(retryTask, scheduleConfig.getRetryInterval(),
scheduleConfig.getRetryInterval(), scheduleConfig.getRetryIntervalUnit());
// 将任务添加到重试任务中
FUTURE_MAP.put(retryTask.taskId, scheduledFuture);
}
- 移除任务
/**
* 根据taskId 移除定时任务
*
* @param taskId 任务id
*/
private void remove(String taskId) {
if (FUTURE_MAP.get(taskId) == null) {
log.error("ScheduledService remove task Failed! {} task not exist", taskId);
return;
}
ScheduledFuture<?> scheduledFuture = FUTURE_MAP.get(taskId);
// 取消当前任务,但并不强制取消正在执行的任务
boolean cancel = scheduledFuture.cancel(false);
if (!cancel) {
log.error("{} task cancel fail", taskId);
}
// 将当前任务从任务队列中删除
FUTURE_MAP.remove(taskId);
}
配置重试参数
通过上面可以知道,重试次数和重试时间间隔,以及最大重试次数和最大重试时间都是配置而来的,这个类定义如下
/**
* <big>定时任务配置类</big>
*
* @author 13684
* @data 2024/5/27 下午5:02
*/
@ConfigurationProperties(prefix = "schedule")
@Component
@Data
public class ScheduleConfig {
// 定时任务最大重试次数
private int maxRetryTimes = 3;
// 定时任务重试时间间隔
private String retryIntervalStr = "MINUTES";
// 定时任务重试时间
private int retryInterval = 1;
// 定时任务重试时间单位
private TimeUnit retryIntervalUnit;
// 定时任务重试最大时间
private String maxRetryIntervalStr = "MINUTES";
// 定时任务重试最大时间
private int maxRetryInterval = 10;
// 定时任务重试最大时间单位
private TimeUnit maxRetryIntervalUnit;
@PostConstruct
public void init() {
for (TimeUnit unit : TimeUnit.values()) {
if (unit.name().equals(retryIntervalStr)) {
retryIntervalUnit = unit;
}
if (unit.name().equals(maxRetryIntervalStr)) {
maxRetryIntervalUnit = unit;
}
}
}
}
快慢线程池
采用双线程池对任务进行处理,执行时间长的和执行时间短的分别处理(可以避免由于长任务占用线程导致频率较高的短任务压爆队列),不过任务第一次运行的时候我们还是用一个执行(因为我们需要记录时间,而不是依靠经验判断),当第二次时进行分流,阈值需要根据项目来估计
快慢线程池(采用了自定义队列和拒绝策略)定义如下:
// 长短任务的区分时间 10s
private static final Long MAX_TIME = 10L;
// 任务执行的默认耗时(用来处理任务第一次运行)
private static final Long DEFAULT_TIME = 100L;
// 记录执行时间,每次记录任务执行时间,用来分类
private static final Map<String, Long> TASK_TIME_MAP = new ConcurrentHashMap<>();
// 快速线程池,用来处理执行时间小于10s的任务(为了区分长任务和短任务)
private static final ThreadPoolExecutor QUICK_HANDLER_EXECUTOR = new ThreadPoolExecutor(2, 4, 30,
TimeUnit.SECONDS, new ResizeLinkedBlockingQueue<>(500),
new ThreadFactoryBuilder().setNameFormat("quickHandler-pool-%d").setDaemon(true).build(),
new ScheduleRejected());
// 标准线程池,执行长任务,并且在开机时执行所有任务并统计时间
private static final ThreadPoolExecutor THREAD_POOL_EXECUTOR = new ThreadPoolExecutor(2, 5, 30,
TimeUnit.SECONDS, new ResizeLinkedBlockingQueue<>(500),
new ThreadFactoryBuilder().setNameFormat("standardHandler-pool-%d").setDaemon(true).build(),
new ScheduleRejected());
在每次任务执行的时候计算时间并且put到map中
// 消费者
private final DConsumer<String, ScheduledRunnable> cff = (taskId, scheduledRunnable) -> {
LocalDateTime startTime = LocalDateTime.now();
log.info("{} task start", taskId);
try {
// 接受任务执行的返回值
Boolean apply = scheduledRunnable.supplier.get();
// 如果执行失败就重试
if (!apply) {
addTask(scheduledRunnable);
}
} catch (Exception e) {
log.error("{} task fail Exception: {}", taskId, e.getMessage(), e);
}finally {
log.info("{} task finish!", taskId);
// 记录当前任务执行时间
TASK_TIME_MAP.put(taskId, LocalDateTimeUtil.between(startTime, LocalDateTime.now()).getSeconds());
}
};
在调用任务执行(这也是整个线程池工具类的入口方法)时进行分流
/**
* 执行重试的定时任务
*
* @param taskId 任务id
* @param supplier 执行任务的方法
*/
public void getSupplier(String taskId, Supplier<Boolean> supplier) {
// 创建重试任务
ScheduledRunnable scheduledRunnable = new ScheduledRunnable(taskId, supplier, LocalDateTime.now().plusSeconds(scheduleConfig.getMaxRetryIntervalUnit().toSeconds(scheduleConfig.getMaxRetryInterval())));
// 如果执行时间小于10s,放到快速线程池中
if (TASK_TIME_MAP.getOrDefault(taskId, DEFAULT_TIME) <= MAX_TIME) {
QUICK_HANDLER_EXECUTOR.execute(() -> cff.accept(taskId, scheduledRunnable));
}else {
THREAD_POOL_EXECUTOR.execute(() -> cff.accept(taskId, scheduledRunnable));
}
}
动态调整线程池参数
在前面采用了我们自定义线程池队列后,加上线程池自带的修改参数的方法,我们现在已经可以修改线程池比较核心的属性,对于线程池参数修改采用web请求的方法进行修改
首先创建一个枚举类,获取项目中所有需要动态调整参数的线程池
public interface BaseEnum {
/**
* 获取一个值。
*
* @return 返回方法的数值。
*/
int getValue();
/**
* 获取描述信息。
*
* @return 返回方法的描述字符串。
*/
String getDesc();
/**
* 检查传入的整数值是否与当前对象的值相等。
*
* @param value 要比较的整数值。
* @return 如果相等返回true,否则返回false。当传入的值为null时,总是返回false。
*/
default boolean equalsValue(Integer value) {
// 检查传入的value是否为null
if (value == null) {
return false;
}
// 比较当前对象的值和传入的值是否相等
return getValue() == value;
}
}
/**
* <big>线程池枚举类</big>
* <p>用来存储项目中所有的线程池</p>
*
* @author 13684
* @data 2024/7/15 上午10:30
*/
public enum SchedulePoolEnum implements BaseEnum {
QuickHandler(0, "快速线程池", ScheduledUtil.getQuickHandlerExecutor()),
ThreadPool(1, "标准线程池", ScheduledUtil.getThreadPoolExecutor());
;
final Integer value;
final String desc;
@Getter
final ThreadPoolExecutor threadPoolExecutor;
SchedulePoolEnum(Integer value, String desc, ThreadPoolExecutor threadPoolExecutor) {
this.value = value;
this.desc = desc;
this.threadPoolExecutor = threadPoolExecutor;
}
@Override
public int getValue() {
return value;
}
@Override
public String getDesc() {
return desc;
}
}
创建web请求实体(ak这里并没有实现,但是项目中一定要鉴权)
/**
* <big>线程池DTO对象</big>
* <p>用来动态修改线程池参数</p>
*
* @author 13684
* @data 2024/7/15 上午10:23
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class SchedulePoolDTO {
// 核心线程数
private int corePoolSize;
// 最大线程数
private int maximumPoolSize;
// 队列容量大小
private int queueCapacity;
// 线程空闲时间
private int keepAliveTime;
// 线程空闲时间单位
private TimeUnit keepAliveTimeUnit;
// 访问id
private String accessId;
// 访问key(鉴权使用)
private String accessKey;
}
最后定义web接口,这里实现了修改线程池参数以及获取线程池信息(可以开发到前端页面上,更加直观)
@Slf4j
@RestController
@RequestMapping("/schedule")
public class ScheduleController implements ApplicationContextAware {
@PutMapping("/updatePool")
public String updatePool(@RequestParam SchedulePoolEnum type, @RequestBody SchedulePoolDTO schedulePoolDTO) {
try {
// 获取线程池
ThreadPoolExecutor threadPoolExecutor = type.getThreadPoolExecutor();
// 设置核心线程数 (这里就直接if判断了,真实项目可以开发前端页面回显)
if (ObjectUtil.isNotEmpty(schedulePoolDTO.getCorePoolSize())) {
threadPoolExecutor.setCorePoolSize(schedulePoolDTO.getCorePoolSize());
}
// 设置最大线程数
if (ObjectUtil.isNotEmpty(schedulePoolDTO.getMaximumPoolSize())) {
threadPoolExecutor.setMaximumPoolSize(schedulePoolDTO.getMaximumPoolSize());
}
// 设置线程空闲时间
if (!ObjectUtil.hasNull(schedulePoolDTO.getKeepAliveTime(), schedulePoolDTO.getKeepAliveTimeUnit())) {
threadPoolExecutor.setKeepAliveTime(schedulePoolDTO.getKeepAliveTime(), schedulePoolDTO.getKeepAliveTimeUnit());
}
// 设置队列容量大小
ResizeLinkedBlockingQueue<Runnable> queue = (ResizeLinkedBlockingQueue<Runnable>) threadPoolExecutor.getQueue();
if (ObjectUtil.isNotEmpty(schedulePoolDTO.getQueueCapacity())) {
queue.setCapacity(schedulePoolDTO.getQueueCapacity());
}
log.info("Update pool success! type={}, schedulePoolDTO={}", type, schedulePoolDTO);
StringBuilder sb = new StringBuilder();
for (SchedulePoolEnum value : SchedulePoolEnum.values()) {
ResizeLinkedBlockingQueue<Runnable> tempQueue = (ResizeLinkedBlockingQueue<Runnable>) value.getThreadPoolExecutor().getQueue();
sb.append(value.getDesc()).append(": corePoolSize=").append(value.getThreadPoolExecutor().getCorePoolSize())
.append(", maximumPoolSize=").append(value.getThreadPoolExecutor().getMaximumPoolSize())
.append(", keepAliveTime=").append(value.getThreadPoolExecutor().getKeepAliveTime(TimeUnit.SECONDS)).append("s")
.append(", queueCapacity=").append(tempQueue.getCapacity())
.append("\n");
}
log.info("All pool info: \n{}", sb);
}catch (Exception e) {
log.error("Update pool failed! type={}, schedulePoolDTO={}", type, schedulePoolDTO);
log.error("Exception ", e);
return "exception";
}
return "success";
}
@GetMapping("/getPool")
public String getPool(@RequestParam SchedulePoolEnum type) {
try {
ThreadPoolExecutor threadPoolExecutor = type.getThreadPoolExecutor();
ResizeLinkedBlockingQueue<Runnable> queue = (ResizeLinkedBlockingQueue<Runnable>) threadPoolExecutor.getQueue();
return type.getDesc() + ": corePoolSize=" + threadPoolExecutor.getCorePoolSize() +
", maximumPoolSize=" + threadPoolExecutor.getMaximumPoolSize() +
", keepAliveTime=" + threadPoolExecutor.getKeepAliveTime(TimeUnit.SECONDS) +
", queueCapacity=" + queue.getCapacity();
}catch (Exception e) {
log.error("Get pool failed! type={}", type);
log.error("Exception ", e);
return "exception";
}
}
}
题外话:主动调用任务
由于此文章的线程池问题出于一个单体服务(定时任务很多),出于业务要求,需要可以同步数据,所以提供一个可以手动调用任务的方法来达到此要求
如果不是单体服务,还是建议引入分布式任务调度框架
这个方法也作为一个web请求来实现,方法传入实体定义如下:
/**
* <big>任务执行的dto对象</big>
* <p>包含主动执行一个任务所需要的属性</p>
*
* @author 13684
* @data 2024/7/15 上午10:18
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class ScheduleTaskDTO {
// 任务全类名
private String className;
// 任务方法名
private String methodName;
// 访问id
private String accessId;
// 访问key(鉴权使用)
private String accessKey;
}
定义一个注解,无作用,专门用来标记当前类的方法可以通过主动调用的方式来执行
/**
* <big>允许主动调用的标识</big>
* <p>不做任何处理,仅仅用来标志,无此标志则不可主动调用</p>
*
* @author 13684
* @data 2024/7/15 上午9:53
*/
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface InitiativeExecute {
}
请求方法定义如下:
/**
* 通过反射,主动调用定时任务(适用于单体架构,无分布式调度框架)
* 需要进行鉴权,根据ak,或者其他方法进行鉴权
*
* @param scheduleTaskDTO 调度任务
* @return 调用结果
*/
@PostMapping("/execute")
public String executeSchedule(@RequestBody ScheduleTaskDTO scheduleTaskDTO) {
try {
// 获取类
Class<?> aClass = Class.forName(scheduleTaskDTO.getClassName());
if (!aClass.isAnnotationPresent(InitiativeExecute.class)) {
return "Class not annotated with @InitiativeExecute";
}
// 获取方法
Method method = aClass.getDeclaredMethod(scheduleTaskDTO.getMethodName());
// 获取访问权限
method.setAccessible(true);
// 获取实例
Object bean = applicationContext.getBean(aClass);
// 执行实例方法
method.invoke(bean);
log.info("Schedule execute success! ScheduleTaskDto={}", scheduleTaskDTO);
} catch (Exception e) {
log.error("Schedule execute failed! ScheduleTaskDto={}", scheduleTaskDTO);
log.error("Exception ", e);
return "exception";
}
return "success";
}
三、写在最后
当前实现的线程池依旧存在不足,如果有更好的实现方法欢迎私信我一起讨论交流。