多线程使用时线程阻塞
1. 背景
因为本人工作是对大量数据进行计算,用户的每个操作都可能引发后台的大量运算。
所以我在后端用了个线程池工具类。
该工具在系统启动是默认创建了一个线程池,该线程池有50个核心线程,4096个阻塞队列,系统中使用多线程的地方都直接调用ThreadPoolUtils.exec(task)或者ThreadPoolUtils.execCallable(Callable<?> task),如下:
public class ThreadPoolUtils {
/**
* 核心线程池大小
*/
public static final int CORE_POOL_SIZE = 50;
/**
* 最大线程池大小
*/
public static final int MAX_POOL_SIZE = 100;
/**
* 阻塞任务队列大小
*/
public static final int QUEUE_CAPACITY = 4096;
/**
* 空闲线程存活时间
*/
public static final Long KEEP_ALIVE_TIME = 60L;
private final static Logger logger = LoggerFactory.getLogger(ThreadPoolUtils.class);
private static final ThreadPoolExecutor EXEC = new ThreadPoolExecutor(
CORE_POOL_SIZE,
MAX_POOL_SIZE,
KEEP_ALIVE_TIME,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(QUEUE_CAPACITY),
new ThreadPoolExecutor.CallerRunsPolicy());
/**
* 新建核心线程池大小
*/
public static final int NEW_CORE_POOL_SIZE = 5;
/**
* 新建最大线程池大小
*/
public static final int NEW_MAX_POOL_SIZE = 10;
/**
* 新建阻塞任务队列大小
*/
public static final int NEW_QUEUE_CAPACITY = 1024;
/**
* 创建一个线程池
*
* @param: task
* @return:
*/
public static ThreadPoolExecutor getNewThreadPool() {
ThreadPoolExecutor exec = new ThreadPoolExecutor(
NEW_CORE_POOL_SIZE,
NEW_MAX_POOL_SIZE,
KEEP_ALIVE_TIME,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(NEW_QUEUE_CAPACITY),
new ThreadPoolExecutor.CallerRunsPolicy());
return exec;
}
/**
* 禁止实例化
*/
private ThreadPoolUtils() {
}
/**
* 提交任务
*/
public static void exec(Runnable task) {
ThreadPoolUtils.catThreadPoolInfo(EXEC);
EXEC.submit(task);
}
/**
* 并行提交任务并获取线程执行后返回的结果
*
* @param: task
* @return:
*/
public static Future<?> execCallable(Callable<?> task) {
ThreadPoolUtils.catThreadPoolInfo(EXEC);
return EXEC.submit(task);
}
/**
* 等待线程执行完成
*
* @param futures
* @return: void
* @date: 2022/4/2 9:49 AM
* @author: slhero
*/
public static void awaitThreadDone(List<Future<?>> futures) {
futures.forEach(f -> {
try {
f.get(10, TimeUnit.MINUTES);
} catch (InterruptedException | ExecutionException e) {
logger.error("多线程等待失败:{}", e.getMessage(), e);
throw new RuntimeException("多线程等待失败!" + e.getMessage());
} catch (TimeoutException e) {
logger.error("多线程等待超时:{}", e.getMessage(), e);
throw new RuntimeException("多线程等待超时!" + e.getMessage());
}
});
}
}
因为在很多业务需要有前后关联关系,但是顺序执行有太耗时,所以这里用到了Future,等待当前业务执行完成后再进行下一步操作。
2. 问题现象
某天发现排队阻塞线程特别多,导致某些业务操作等待很久,都没有得到返回,从后台日志看,将当前操作丢入线程后,等了20分钟才分配到线程进行计算。
初步分析,是业务量太多,导致后台计算不过来,查看CPU使用不到10%,服务器的资源还没有完全被使用,应该不是业务操作计算堆积。
进一步查看代码,发现代码中,存在几处类似如下的代码:
List<Future<?>> futureList = new ArrayList<>(lists.size());
for (List resultVo : lists) {
Future<?> future = ThreadPoolUtils.execCallable(() -> {
// do something
return true;
});
futureList.add(future);
}
ThreadPoolUtils.awaitThreadDone(futureList);
这个地方的作用是,新启线程执行业务,并等待业务返回后执行下一步操作。
本来没什么问题,但是ThreadPoolUtils.execCallable方法占用了系统默认线程池中的线程。
导致其他地方的再想使用线程时,必须等待该方法执行完成。
而该方法处于循环中,存在大量数据调用该方法,从而导致业务线程等了许久才执行。
3.解决方案
禁止使用系统默认线程池做循环多线程等待的业务,改用新建线程池方式,最终一定要关闭线程池:
ThreadPoolExecutor pool = ThreadPoolUtils.getNewThreadPool();
try {
List<Future<?>> futureTasks = new ArrayList<>(lists.size());
for (List result : keys) {
Future<?> futureTask = pool.submit(() -> {
// do somethin
}
return true;
});
futureTasks.add(futureTask);
}
ThreadPoolUtils.awaitThreadDone(futureTasks);
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
try {
if (pool != null && !pool.isShutdown()){
pool.shutdown();
}
} catch (Exception e) {
logger.error("关闭线程池失败:{}", e.getMessage(), e);
}
}
4.总结
系统共用的工具类、公共方法,大量调用时一定要注意是否会影响其他业务.