最近在测试的时候由于业务需要,需要对系统的并发进行控制,因为之前是线程池的方式,多线程处理任务。但是需要每次调整后都要发版,很麻烦。所以采用动态的线程池进行动态修改线程数量,从而达到控制并发处理的目的。参考美团的技术文章,介绍主要的技术点。
1、线程池基础知识
线程池的代码如图所示,创建时需要这些参数,分别简单介绍下每个参数的含义。
1.corePoolSize:the number of threads to keep in the pool, even if they are idle, unless {@code allowCoreThreadTimeOut} is set
(核心线程数大小:不管它们创建以后是不是空闲的。线程池需要保持 corePoolSize 数量的线程,除非设置了 allowCoreThreadTimeOut。)
2.maximumPoolSize:the maximum number of threads to allow in the pool。
(最大线程数:线程池中最多允许创建 maximumPoolSize 个线程。)
3.keepAliveTime:when the number of threads is greater than the core, this is the maximum time that excess idle threads will wait for new tasks before terminating。
(存活时间:如果经过 keepAliveTime 时间后,超过核心线程数的线程还没有接受到新的任务,那就回收。)
4.unit:the time unit for the {@code keepAliveTime} argument
(keepAliveTime 的时间单位。)
5.workQueue:the queue to use for holding tasks before they are executed. This queue will hold only the {@code Runnable} tasks submitted by the {@code execute} method。
(存放待执行任务的队列:当提交的任务数超过核心线程数大小后,再提交的任务就存放在这里。它仅仅用来存放被 execute 方法提交的 Runnable 任务。)
6.threadFactory:the factory to use when the executor creates a new thread。
(线程工程:用来创建线程工厂。比如这里面可以自定义线程名称,当进行虚拟机栈分析时,看着名字就知道这个线程是哪里来的,不会懵逼。)
7.handler :the handler to use when execution is blocked because the thread bounds and queue capacities are reached。
(拒绝策略:当队列里面放满了任务、最大线程数的线程都在工作时,这时继续提交的任务线程池就处理不了,应该执行怎么样的拒绝策略。)
这样一定要清楚的一点是:maxzsize+queueSize是能够处理的任务最大数。任务队列满了才会触发maxSize的里面的多余线程去处理。
2、为什么要动态线程
最近工作中发现,并发处理任务的时候一般都用线程池,但是如果按照 IO 密集型任务或者 CPU 密集型任务 等按照网上的算法设置一些参数,虽然可以,但是不可控,万一峰值来了或者下游服务处理不过来,需要增加,或者减少,coreSize,难道每次都要改代码发布吗?所以动态配置变得很重要,让程序的处理能力变得可控。
尽管经过谨慎的评估,仍然不能够保证一次计算出来合适的参数,那么我们是否可以将修改线程池参数的成本降下来,这样至少可以发生故障的时候可以快速调整从而缩短故障恢复的时间呢? 基于这个思考,我们是否可以将线程池的参数从代码中迁移到分布式配置中心上,实现线程池参数可动态配置和即时生效,线程池参数动态化前后的参数修改流程对比如下。
直接上代码:
@Component
public class DynamicExcutorFactory {
private static final Logger log = LoggerFactory.getLogger(DynamicExcutorFactory.class);
private static ThreadPoolExecutor executor;
private static final String CORE_SIZE = "task.coresize";
private static final String MAX_SIZE = "task.maxsize";
public DynamicExcutorFactory() {
Config config = ConfigService.getConfig("application");
int corePoolSize = config.getIntProperty(CORE_SIZE,10);
int maximumPoolSize = config.getIntProperty(MAX_SIZE,20);
init(corePoolSize,maximumPoolSize);
}
/**
* 初始化
*/
private void init(int corePoolSize,int maximumPoolSize) {
log.info("init core:{},max:{}",corePoolSize,maximumPoolSize);
if (executor == null) {
synchronized (DynamicExcutorFactory.class) {
executor = new ThreadPoolExecutor(corePoolSize, maximumPoolSize,60L, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(50), new ThreadFactoryBuilder().setNameFormat("my-thread-%d").build(), new ThreadPoolExecutor.CallerRunsPolicy());
}
}
}
/**
* 监听器
*/
@ApolloConfigChangeListener
private void listen(ConfigChangeEvent changeEvent) {
if (changeEvent.isChanged(CORE_SIZE)) {
ConfigChange change = changeEvent.getChange(CORE_SIZE);
String newValue = change.getNewValue();
refreshThreadPool(CORE_SIZE, newValue);
log.info("核心线程发生变化key={},oldValue={},newValue={},changeType={}", change.getPropertyName(), change.getOldValue(), change.getNewValue(), change.getChangeType());
}
if (changeEvent.isChanged(MAX_SIZE)) {
ConfigChange change = changeEvent.getChange(MAX_SIZE);
String newValue = change.getNewValue();
refreshThreadPool(MAX_SIZE, newValue);
log.info("最大线程数发生变化key={},oldValue={},newValue={},changeType={}", change.getPropertyName(), change.getOldValue(), change.getNewValue(), change.getChangeType());
}
}
/**
* 刷新线程池
*/
private void refreshThreadPool(String key, String newValue) {
if (executor == null) {
return;
}
if (CORE_SIZE.equals(key)) {
executor.setCorePoolSize(Integer.parseInt(newValue));
log.info("修改核心线程数 key={},value={}", key, newValue);
}
if (MAX_SIZE.equals(key)) {
executor.setMaximumPoolSize(Integer.parseInt(newValue));
log.info("修改最大线程数 key={},value={}", key, newValue);
}
}
public ThreadPoolExecutor getExecutor(){
return executor;
}
}
写个方法去测试就可以了
private void process(List<Long> list) {
List<List<Long>> parts = Lists.partition(list,3);
if (CollectionUtils.isNotEmpty(parts)) {
CountDownLatch latch = new CountDownLatch(parts.size());
LOGGER.info("parts.size={}", parts.size());
executor = excutorFactory.getExecutor();
for (List<Long> part : parts) {
executor.submit(() -> {
try {
System.out.println("this is a thread.");
Thread.sleep(2000);
}catch (Exception e){
LOGGER.error("error:",e);
} finally {
latch.countDown();
}
});
}
try {
latch.await();
} catch (InterruptedException interruptedException){
LOGGER.error("InterruptedException error:",interruptedException);
Thread.currentThread().interrupt();
} catch (Exception e) {
LOGGER.error("ConutDownLaunch error:", e);
}
}
}
这样每次在Apollo里面调整coreSize的时候,就会被监听到,然后会动态增加coreSize,
因为线程池提供了set方法,所以我们可以动态变更。
1.首先是参数合法性校验。
2.然后用传递进来的值,覆盖原来的值。
3.判断工作线程是否是大于最大线程数,如果大于,则对空闲线程发起中断请求。
PS:设置的coreSize最大不能超过maxSize。即使超过也不生效。最大也是MaxSize的线程数。如果想扩大的化可以将maxSize也增加。
在这个方法中我们可以看到,如果工作线程数大于最大线程数,则对工作线程数量进行减一操作,然后返回 null。
所以,这个地方的实际流程应该是: 创建新的工作线程 worker,然后工作线程数进行加一操作。 运行创建的工作线程 worker,开始获取任务 task。 工作线程数量大于最大线程数,对工作线程数进行减一操作。 返回 null,即没有获取到 task。 清理该任务,流程结束。
这样一加一减,所以真正在执行任务的工作线程数的数量一直没有发生变化,也就是最大线程数。
减少的时候你会发现coreSize减少了,但是活跃的线程数并没有变。这是因为maxSize没有减少,如果想变动的话,将maxSize也跟着减少,活跃线程数就会降下来了。
但是想动态配置Queue的大小,发现不能set,因为容量是final修饰的。不能修改。这个时候就可以自己copy一份代码,自定义一个Queue,然后把capacity设置成非final修饰的,增加一个set方法即可。
copy修改后:
到此为止就可以做成了动态的线程池了,这些都是参考美团的实践。自己可以跟美团一样做个配置页面就行了。
3、相关知识查漏补缺
查询资料里面也有一些知识点贴到下面,大家可以顺便看看自己知道不知道,查漏补缺:
问题一:线程池被创建后里面有线程吗?如果没有的话,你知道有什么方法对线程池进行预热吗?
回答:线程池被创建后如果没有任务过来,里面是不会有线程的。如果需要预热的话可以调用下面的两个方法:
全部启动:
仅启动一个:
问题二:核心线程数会被回收吗?需要什么设置?
回答:核心线程数默认是不会被回收的,如果需要回收核心线程数,需要调用下面的方法:
allowCoreThreadTimeOut 该值默认为 false。