业务场景
《员工成长》系统中,有很多的配置页面和输入录入页面,有的影响员工的KPI的正常计算,有的影响奖金的计算等等。为了避免使用者忘记配置这些前置数据,从而造成KPI和奖金计算错误带来的后续不必要的工作,也为了保证系统正常运行,我们需要开发一个自检页面,供相关人员查询和监督这些数据的配置情况。
成本页面:

系统设计
主要讲优化,不讲设计,所以直接简述设计方案
该功能主要采用 策略模式 + 线程池来实现。
- 策略模式:使用策略模式主要是将各个检测项目进行拆分,方便后续扩展
- 线程池:主要是为多个策略同时检测的时候提速
慢接口
阿里云日志:
上线后,从阿里云日志数据统计图中可以看到该检测接口直接飙到900ms左右的响应时间。
第一次优化
思路
1、SQL执行太慢
2、线程池太慢
复现
PINPOINT复现截图:
在测试环境进行复现后,到PINPOINT监控中查看请求链路(上图)。发现SQL查询并不慢,基本上在50ms左右都可以返回。到这里基本可以确定不是SQL的问题,那么压力就给到线程池这边了。
查找问题
/**
* 默认线程池大小
*/
private static final Integer DEFAULT_POOL_SIZE = 6;
/**
* 初始化一个线程池
*
* @param poolName 池名称
* @param poolSize 池大小
* @return ExecutorService 线程池服务
*/
private static ExecutorService init(String poolName) {
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(poolSize, DEFAULT_POOL_SIZE,
1L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<>(32),
new ThreadFactoryBuilder().setNameFormat("Pool-" + poolName).setDaemon(false).build(),
new ThreadPoolExecutor.CallerRunsPolicy());
threadPoolExecutor.allowCoreThreadTimeOut(true);
// JVM钩子
Runtime.getRuntime().addShutdownHook(
new Thread(new FutureTask<Void>(() -> {
threadPoolExecutor.shutdown();
log.info(ThreadPoolsUtil.class, new Object[]{},
String.format("钩子[%s]正在关闭线程池[%s]", Thread.currentThread().getName(), poolName));
return null;
})));
return threadPoolExecutor;
}
可以看到线程池的核心线程数使用了默认大小(DEFAULT_POOL_SIZE = 6),由于本次的检测策略数量远远超过这个值(本次策略增加到了10个以上),很大概率就是由于线程池核心不足,将其他的处理请求放到了阻塞队列。
解决方案
直观解决方案:增加核心线程大小
/**
* 初始化一个线程池
*
* @param poolName 池名称
* @param poolSize 池大小
* @return ExecutorService 线程池服务
*/
private static ExecutorService init(String poolName, int poolSize) {
final int poolSizeDealt = poolSize < DEFAULT_POOL_SIZE ? DEFAULT_POOL_SIZE : poolSize;
// poolSizeDealt * 1.25
int maxPoolSizeDealt = poolSizeDealt + (poolSizeDealt >> 2);
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(poolSizeDealt, maxPoolSizeDealt,
1L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<>(poolSizeDealt << 1),
new ThreadFactoryBuilder().setNameFormat("Pool-" + poolName).setDaemon(false).build(),
new ThreadPoolExecutor.CallerRunsPolicy());
threadPoolExecutor.allowCoreThreadTimeOut(true);
// JVM钩子
Runtime.getRuntime().addShutdownHook(
new Thread(new FutureTask<Void>(() -> {
threadPoolExecutor.shutdown();
log.info(ThreadPoolsUtil.class, new Object[]{},
String.format("钩子[%s]正在关闭线程池[%s]", Thread.currentThread().getName(), poolName));
return null;
})));
return threadPoolExecutor;
}
由于我们开放了核心线程的设置,所以直接在调用的地方修改即可,这次直接修改到16
public static final String POOL_NAME = "BUSINESS_DATA_EXCEPTION_CHECK_POOL";
public static final Integer POOL_SIZE = 2 << 3;
ExecutorService executorService = ThreadPoolsUtil.getOrInitExecutors(POOL_NAME, POOL_SIZE);
修改后性能有所提升(400ms左右),但是还有提升空间
第二次优化
问题:既然核心线程打满,其他线程会被挂到队列中,那么是不是可以优化一下队列呢?
查看一下线程池支持的队列:
- ArrayBlockingQueue:这是一个基于数组的有界队列。它按照先进先出(FIFO)的顺序存储元素,并且在队列已满时会阻塞添加操作。
- LinkedBlockingQueue:这是一个基于链表的有界队列。它按照先进先出(FIFO)的顺序存储元素,并且在队列已满时会阻塞添加操作。
- SynchronousQueue:这是一个没有任何容量的队列。它在插入操作时会等待相应的删除操作,反之亦然。因此,它通常用于直接将生产者和消费者线程进行同步。
- PriorityBlockingQueue:这是一个支持优先级的无界队列。它按照元素的优先级进行存储,并且在插入操作时会根据优先级进行排序。
- DelayQueue:这是一个支持延迟元素的无界队列。它按照延迟时间进行存储,并且只有在延迟时间到达时才可以获取元素。
从特性可以分析出:DelayQueue 和 PriorityBlockingQueue 者两种队列并不是我需求所需要的队列,直接排除,那么就剩下2种队列了(LinkedBlockingQueue已经使用过了)。
问题:在此处ArrayBlockingQueue和LinkedBlockingQueue,那种数据结构性能更好?
ArrayBlockingQueue
和LinkedBlockingQueue
都是常见的Java队列实现,但它们的性能和适用场景略有不同。
ArrayBlockingQueue
是一个有界阻塞队列,内部使用一个定长的数组作为缓冲区。由于数组长度固定,因此在队列已满时,新的插入操作将被阻塞,直到队列中的元素被消费或者队列被清空。ArrayBlockingQueue
适用于在生产者和消费者之间进行通信,因为它可以确保先进先出(FIFO)的顺序,并且在添加元素时不需要进行任何内存分配或移动操作。
相比之下,LinkedBlockingQueue
是一个基于链表的无界阻塞队列,可以动态地调整大小以容纳任意数量的元素。在队列已满时,新的插入操作将被阻塞,直到队列中的元素被消费或者队列被清空。由于LinkedBlockingQueue
的容量不受限制,因此它适用于那些不知道需要存储多少元素的场景。但是,在大量添加或删除元素时,由于链表结构的特性,LinkedBlockingQueue
的性能可能会受到一定的影响。
在给定代码中,由于ThreadPoolExecutor
的核心线程数和最大线程数都已经确定,因此队列的容量也可以确定。在这种情况下,ArrayBlockingQueue
是一个更好的选择,因为它可以避免链表结构的性能影响,并且在添加元素时不需要进行任何内存分配或移动操作。
private static ExecutorService init(String poolName, int poolSize) {
final int poolSizeDealt = poolSize < DEFAULT_POOL_SIZE ? DEFAULT_POOL_SIZE : poolSize;
// poolSizeDealt * 1.25
int maxPoolSizeDealt = poolSizeDealt + (poolSizeDealt >> 2);
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(poolSizeDealt, maxPoolSizeDealt,
1L, TimeUnit.MILLISECONDS,
new ArrayBlockingQueue<>(poolSizeDealt << 1),
new ThreadFactoryBuilder().setNameFormat("Pool-" + poolName).setDaemon(false).build(),
new ThreadPoolExecutor.CallerRunsPolicy());
threadPoolExecutor.allowCoreThreadTimeOut(true);
// JVM钩子
Runtime.getRuntime().addShutdownHook(
new Thread(new FutureTask<Void>(() -> {
threadPoolExecutor.shutdown();
log.info(ThreadPoolsUtil.class, new Object[]{},
String.format("钩子[%s]正在关闭线程池[%s]", Thread.currentThread().getName(), poolName));
return null;
})));
return threadPoolExecutor;
}
到此,第二次优化已经OK,上线测试,接口降到了200ms左右
第三次优化
其实200ms的接口,并不算快,还有优化的空间
分析

从上图我们可以知道线程池处理线程的流程:
1、当小于核心线程数的时候,直接创建
2、大于核心线程数,但小于队列容量,放入队列
3、拒绝策略
思考
我们是否能够将放入队列这个步骤跳过呢?
只要不超过最大线程数就无脑新建线程,这样就可以避开放入队列中进行排队的耗时,但是当线程池的最大线程数都已经满足不了需求的时候就会报错,这也是我们不想看到的。
能满足我们这里的两点需求的只有一种方法了:颠倒线程池放入阻塞队列和创建线程的顺序
实现
采用同步队列(SynchronousQueue)
如果希望颠倒线程池放入阻塞队列和创建线程的顺序,可以考虑使用SynchronousQueue
来实现。SynchronousQueue是一个没有容量限制的阻塞队列,生产者线程在插入元素时必须等待消费者线程取走该元素,反之亦然。这种队列可以实现线程池在提交任务时不创建线程,而是直接将任务交给空闲的线程执行。
private static ExecutorService init(String poolName, int poolSize) {
final int poolSizeDealt = poolSize < DEFAULT_POOL_SIZE ? DEFAULT_POOL_SIZE : poolSize;
// poolSizeDealt * 2
int maxPoolSizeDealt = poolSizeDealt << 1;
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(poolSizeDealt, maxPoolSizeDealt,
1L, TimeUnit.SECONDS,
new SynchronousQueue<>(),
new ThreadFactoryBuilder().setNameFormat("Pool-" + poolName).setDaemon(false).build(),
new ThreadPoolExecutor.CallerRunsPolicy());
threadPoolExecutor.allowCoreThreadTimeOut(true);
// JVM钩子
Runtime.getRuntime().addShutdownHook(
new Thread(new FutureTask<Void>(() -> {
threadPoolExecutor.shutdown();
log.info(ThreadPoolsUtil.class, new Object[]{},
String.format("钩子[%s]正在关闭线程池[%s]", Thread.currentThread().getName(), poolName));
return null;
}
))
);
return threadPoolExecutor;
}
SynchronousQueue还可以实现公平锁和非公平锁两种策略。由于业务并没有这方面的需求,而且使用公平锁会有一点性能损耗,可以直接忽略这部分的特性,保持默认即可。
评论区