引言
工作中难免会遇到各种并发场景,笔者先后经历的公司中,很多同事或多或少都用到过Java线程池来实现并发处理。
但线程池用的好的,却没几个。笔者也曾排查过线上问题,有几次,都是因为线程泄漏,导致出现无法创建更多线程,甚至JVM垃圾回收一直无法成功,最终引发故障。
今天给大家分享一下,实际工作中,线程池应该如何用,才是最正确的。
一、线程池使用几个重要原则
原则1:禁止在请求到达,执行方法中创建线程池。
释义:请求并发高的时候,一个请求创建一个线程池,服务器线程资源会被瞬间打垮。线程池的创建,是一个很重的操作,且容易出现线程泄漏。参看本文【附1:线程池泄漏案例】
原则2:线程池创建,尽量使用Spring 容器来管理其生命周期,当JVM关闭时,同步关闭线程池。
释义:防止线程池hold住JVM进程,导致Spring 容器已经关闭,但java 进程还在,造成应用假活现象。
原则3:线程池创建,应该使用自定义线程创建工厂类,定制线程创建名称。
释义:使用自定义创建工厂类,定制线程名称生成规则,可方便线上排查问题时,诊断线程池的运行情况。
原则4:业务并发不高的场景,禁止擅自创建线程池,应该考虑复用现有业务线程池。
释义:并发不高的场景,通常情况,不过是想利用并行处理,或者异步处理的机制。非必要,不应该创建更多线程池。否则线程池会出现滥用情况,越来越不可控。
二、自定义线程池创建示例
集中创建,便于集中管理,Spring 容器关闭时,自动将线程池也关闭掉。
@Configuration
@EnableAsync
public class BizExcutorConfig {
private static final int defaultThreadNum = Runtime.getRuntime().availableProcessors() * 2;
public final static List<ThreadPoolExecutor> DEMO_EXECUTORS = Lists.newArrayList();
/**
* 通用业务线程池-适合并发小的业务
* 多个业务可以共用此线程池
* */
@Bean
public ThreadPoolExecutor commonBizExecutor(){
ThreadFactory threadFactory = new ThreadFactory() {
// 自定义线程编号
final AtomicInteger threadIndex = new AtomicInteger(1);
@Override
public Thread newThread(Runnable r) {
// 自定义线程名称
return new Thread(r, "commonBiz-" + threadIndex.getAndIncrement());
}
};
ThreadPoolExecutor executor = new ThreadPoolExecutor(
// 自定义核心线程数
defaultThreadNum,
// 自定义最大线程数
defaultThreadNum,
// 自定义线程最大空闲时间
60,
// 自定义线程最大空闲时间-单位
TimeUnit.SECONDS,
// 自定义线程繁忙时-缓存任务队列
new ArrayBlockingQueue<>(100),
// 自定义线程创建工厂类
threadFactory,
// 自定义线程池繁忙时-拒绝策略-抛出异常由主线程处理
new ThreadPoolExecutor.AbortPolicy()
);
// 加入线程池容器,JVM关闭时,自动销毁线程池
DEMO_EXECUTORS.add(executor);
return executor;
}
@PreDestroy
public void closeThreadPool(){
/** 线程池在JVM关闭后-应当自销毁 */
for (ThreadPoolExecutor demoExecutor : DEMO_EXECUTORS) {
demoExecutor.shutdown();
}
}
}
三、线程池泄漏案例
该案例存在的问题是:在main方法中创建线程池,方法已经按预期结束了,但是线程池仍然活跃。
public static void main(String[] args) throws IOException, IllegalAccessException, InterruptedException {
final List<Integer> poolNo = Lists.newArrayList(1, 2, 3, 4, 5);
for (int i = 0; i < 5; i++) {
System.out.println("第 " + i + " 个线程池创建。");
long start = System.currentTimeMillis();
System.out.println(System.currentTimeMillis());
final Integer seq = poolNo.get(i);
ThreadFactory threadFactory = new ThreadFactory() {
// 自定义线程编号
final AtomicInteger threadIndex = new AtomicInteger(1);
@Override
public Thread newThread(Runnable r) {
// 自定义线程名称
return new Thread(r, "test" + seq + "-pool-" + threadIndex.getAndIncrement());
}
};
ThreadPoolExecutor executor = new ThreadPoolExecutor(
// 自定义核心线程数
5,
// 自定义最大线程数
10,
// 自定义线程最大空闲时间
60,
// 自定义线程最大空闲时间-单位
TimeUnit.SECONDS,
// 自定义线程繁忙时-缓存任务队列
new ArrayBlockingQueue<>(100),
// 自定义线程创建工厂类
threadFactory,
// 自定义线程池繁忙时-拒绝策略-抛出异常由主线程处理
new ThreadPoolExecutor.AbortPolicy()
);
System.out.println(System.currentTimeMillis());
System.out.println("耗时:" + (System.currentTimeMillis() - start));
executor.execute(new Runnable() {
@Override
public void run() {
System.out.println("线程提交任务1");
}
});
executor.execute(new Runnable() {
@Override
public void run() {
System.out.println("线程提交任务2");
}
});
executor.execute(new Runnable() {
@Override
public void run() {
System.out.println("线程提交任务3");
}
});
executor.execute(new Runnable() {
@Override
public void run() {
System.out.println("线程提交任务4");
}
});
executor.execute(new Runnable() {
@Override
public void run() {
System.out.println("线程提交任务5");
}
});
}
Thread.currentThread().sleep(360000000000000L);
}
main方法创建线程池后,线程池没有任何引用,但是线程池一直存活,并不会被垃圾回收。
查看进程线程栈,可以清晰看到,当前线程池的线程并没有被回收。
最后,码字不易,如有不尽事宜之处,欢迎留言咨询,或微信搜索:java高手真经
联系作者,一起探讨交流。