前言:
不知道大家在使用线程池的时候,是否存在以下的疑惑,这是我看到的某些同事的代码,他们是这样使用线程池的:
1、 线程池局部变量,new出来使用,没有手动shutdown
2、 线程池局部变量,new出来使用,并且最后手动shutdown
3、 线程池定义为static类型,进行类复用
大家先想想到底哪种方式是正确的,以及错误的方式可能会带来什么问题,让我们带着疑问去看这篇文章。
本文探索了局部变量线程池、类变量线程池使用上的坑,和可能导致的问题。最后根据经验和实际上线的项目,给出当前我的线程池使用经验,以及某些创建线程池的变化,供大家学习。
方式一:线程池局部使用,没有shutdown
首先,我们明确:局部变量new出来的线程池,执行这段代码的程序的每一个线程都会去创建一个局部的线程池。暂且不说每一个线程都去创建线程池是出于什么神奇的目的,首当其冲的线程池的复用的性质就被打破了。创建出来的线程池都得不到复用,那么还有什么必要花费大精力创建线程池?
所以线程池局部使用本身就是不推荐的使用方式!
其次,我们再来想想,局部使用线程池,同时设置核心线程不为0,且设置allowCoreThreadTimeOut=false(空闲后不回收核心线程池)会导致什么问题?(想都不用想,核心线程池得不到回收,自然会导致OOM)
以下是问题代码:
public static void main(String[] args) {
while (true) {
try {
//newFixedThreadPool不会回收核心线程 可能导致OOM
ExecutorService service = Executors.newFixedThreadPool(1);
service.submit(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(2000); 模拟处理业务
} catch (InterruptedException e) {
}
}
});
service = null;
} catch (Exception e) {
}
try {
Thread.sleep(2000);
System.gc();
} catch (InterruptedException e) {
}
}
}
那么,是否我们设置核心线程可以回收不就好咯?同样会出现问题。系统可能会根据你设置的线程过期时间,呈现有规律的内存占用上升,然后下降,然后又上升,然后又下降的趋势。你说说这是好的内存运行情况?
方式二:线程池局部使用,使用完后手动shutdown线程池
okok,这种方式OOM的风险降低了,但是又是局部使用局部使用,你干嘛要局部使用线程池呢?这样不就使得每一个线程都会new一个线程池,导致线程池不会复用,这和你不用线程池有什么区别呢?系统还白白花费资源去创建线程池。
2024.12.24 修订补充:其实是存在局部使用线程池的情况,例如隔离批量状态查询任务,需要任务隔离,避免任务相互干扰的情况,可以使用局部线程池
方式三:线程池定义为static类型,进行类复用
明显,到这里才是正确的使用线程池的方式。static修饰的类变量只会加载一次,所有的线程共享这一个线程池了呗。以下是正确的使用代码:
/**
* @author: 代码丰
* @Description:
*/
public class staticTestExample {
//static 定义线程池
private static ThreadPoolExecutor pool = new ThreadPoolExecutor(
corePoolSize,
maximumPoolSize,
timeout,
TimeUnit.SECONDS,
new LinkedBlockingDeque<>(),
new ThreadPoolExecutor.AbortPolicy());
public static void main(String[] args) {
//使用线程池
Future<Boolean> submit = pool.submit(() -> true);
}
}
业务中的线程池的复用
这里给出以下两种思路
第一种:根据业务执行类型去创建线程池(同一类型的业务复用一个线程池)
简单来说,业务场景相同,且需要用到线程池的地方,复用一个线程池。比如,拆分任务场景,一次性需要同时拆分100个任务去执行,就可以把这100个相同业务场景的任务交给一个特定命名的线程池处理。这个线程池就是专门去处理任务拆分的。
代码如下:
/**
* @author 代码丰
* 创建线程池工具类
*/
public class ThreadPoolUtil {
private static final Map<String, ExecutorService> POOL_CATCH = Maps.newConcurrentMap();
/**
* 根据特定名称去缓存线程池
* @param poolName
* @return
*/
public static ExecutorService create(String poolName) {
//map有缓存直接复用缓存中存在的线程池
if (POOL_CATCH.containsKey(poolName)) {
return POOL_CATCH.get(poolName);
}
// synchronized锁字符串常量池字段 防止并发,使得map中缓存的线程池,只会创建一次
synchronized (poolName.intern()) {
int poolSize = Runtime.getRuntime().availableProcessors() * 2;
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(poolSize, poolSize,
30L, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>(),new ThreadPoolExecutor.AbortPolicy());
POOL_CATCH.put(poolName, threadPoolExecutor);
return threadPoolExecutor;
}
}
}
/**
* @author: 代码丰
*/
public class CorrectTest {
public static void main(String[] args) {
ExecutorService pool = ThreadPoolUtil.create("拆分任务线程池");
//线程池执行任务
Future<Boolean> submit = pool.submit(() -> true);
}
}
第二种:根据用户登陆性质去创建线程池(用一类型的用户复用一个线程池)
简单来说,用户类型且业务场景相同,需要用到线程池的地方,复用一个线程池。
/**
* @author: 代码丰
* @Description:
*/
public class CorrectTest {
public static void main(String[] args) {
//模拟得到用户信息
Userinfo = getUserinfo();
//模拟用相同的用户类型(type)去创建线程池
ExecutorService pool = ThreadPoolUtil.create(Userinfo.getType);
//线程池执行任务
Future<Boolean> submit = pool.submit(() -> true);
}
}
总结
- 使用全局线程池而不是局部线程池,否则可能会有连续创建局部线程池的OOM风险
- 就算使用局部线程池,最后一定要shutdown,否则可能导致不回收核心线程的内存泄漏
- 理解线程池是为了复用的,不要代码中随意new一个局部线程池