本文包含知识点
- 为什么需要使用线程池?
- 线程池的创建及重要参数
- springboot中使用线程池
1.为什么需要线程池?
java中经常需要用到多线程来处理一些业务,我们非常不建议单纯使用继承Thread或者实现Runnable接口的方式来创建线程,那样势必有创建及销毁线程耗费资源、线程上下文切换问题。同时创建过多的线程也会占用过多的内存,这个时候引入线程池比较合理,方便线程任务的管理,比如接口主线程不需要等待子线程逻辑执行完就马上返回。java中涉及到线程池的相关类均在jdk1.5开始的java.util.concurrent包中,涉及到的几个核心类及接口包括:Executor、Executors、ExecutorService、ThreadPoolExecutor、FutureTask、Callable、Runnable等。
先来个实际业务场景demo:用户在中国移动积分商城下单,在支付积分后回调我们的接口给用户发送影视会员时长,其中我们就采用了多线程的方式给“客户发送短信(兑换码)" 并 "提交虚拟码(流水号等信息)给中国移动积分商城",流程如下图(只截取了部分)

伪代码思路如下:
@ResponseBody
@RequestMapping(value = "/notifyOrder", method = RequestMethod.POST)
public CommonResult notifyOrder(@RequestParam(value = "req", required = false) String req) {
log.info("notifyOrder method parameter:req={}", req);
CommonResult result = new CommonResult();
// 1、校验工作
// 2、decode送过来的原始数据并序列化
PointsMallOrderInfo orderInfo = StringToBean(req, "data", PointsMallOrderInfo.class);
// 3、保存推送过来的原始数据
orderInfoService.addOrderInfo(orderInfo);
// 4、获取系统中状态为可用的虚拟码
// 5、多线程异步处理 发送短信、提交虚拟码
AsynSendAndSubmitTask.asynSendAndSubmit(orderInfo);
// 6、持久化虚拟码流水号等信息
return result;
}
public class AsynSendAndSubmitTask {
private static ExecutorService asynSendAndSubmitThreadPool = new ThreadPoolExecutor(10, 10, 60L, TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(40));
public static void asynSendAndSubmit(final PointsMallOrderInfo orderInfo) {
asynSendAndSubmitThreadPool.execute(new Runnable() {
@Override
public void run() {
// 短信下发虚拟码给用户
Long messageId = smsService.sendSms(new SendSmsInfo());
// 给积分商城提交虚拟码
String submitResult = HttpUtil.httpPost(false, params, SETVIRTUALCODE_URL);
}
});
}
}
程序逻辑主线程中执行“保存推送过来的基础数据、查询并取出可用的充值码、持久化充值码及相关流水号信息操作,但"提交虚拟码"和"短信发送虚拟码"则以多线程的方式异步处理,以加快接口响应。(当然也可以用kafka/MQ之类的消息队列替代)。
2.线程池的创建及重要参数
线程池可以自动创建也可以手动创建,自动创建体现在Executors工具类中,常见的可以创建newFixedThreadPool、newCachedThreadPool、newSingleThreadExecutor、newScheduledThreadPool;手动创建体现在可以灵活设置线程池的各个参数,体现在代码中即ThreadPoolExecutor类构造器上各个实参的不同:
public static ExecutorService newFixedThreadPool(int var0) {
return new ThreadPoolExecutor(var0, var0, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue());
}
public static ExecutorService newSingleThreadExecutor() {
return new Executors.FinalizableDelegatedExecutorService(new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue()));
}
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, 2147483647, 60L, TimeUnit.SECONDS, new SynchronousQueue());
}
public static ScheduledExecutorService newScheduledThreadPool(int var0) {
return new ScheduledThreadPoolExecutor(var0);
}
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {……}
ThreadPoolExecutor中重要的几个参数详解
- corePoolSize:核心线程数,也是线程池中常驻的线程数,线程池初始化时默认是没有线程的,当任务来临时才开始创建线程去执行任务
- maximumPoolSize:最大线程数,在核心线程数的基础上可能会额外增加一些线程,需要注意的是只有当workQueue队列填满时才会创建多于corePoolSize的线程(线程池总线程数不超过maxPoolSize)
- keepAliveTime:如果线程池当前的线程数大于corePoolSize,那么多余的线程空闲时间超过keepAliveTime就会被自动终止回收掉,注意当corePoolSize=maxPoolSize时,keepAliveTime参数也就不起作用了(因为不存在非核心线程);
- unit:keepAliveTime的时间单位
- workQueue:用于保存任务的队列,可以为无界、有界、同步移交三种队列类型之一,当池子里的工作线程数大于corePoolSize时,这时新进来的任务会被放到队列中
- threadFactory:创建线程的工厂类,默认使用Executors.defaultThreadFactory(),也可以使用guava库的ThreadFactoryBuilder来创建
- handler:线程池无法继续接收任务(队列已满且线程数达到maximunPoolSize)时的饱和策略,取值有AbortPolicy、CallerRunsPolicy、DiscardOldestPolicy、DiscardPolicy
线程池中的线程创建流程图

举个栗子:现有一个线程池,corePoolSize=10,maxPoolSize=20,队列长度为100,那么当任务过来会先创建10个核心线程数,接下来进来的任务会进入到队列中直到队列满了,会创建额外的线程来执行任务(最多20个线程),这个时候如果再来任务就会执行拒绝策略。
workQueue队列
- SynchronousQueue(同步移交队列):队列不作为任务的缓冲方式,可以简单理解为队列长度为零
- LinkedBlockingQueue(无界队列):队列长度不受限制,当请求越来越多时(任务处理速度跟不上任务处理速度造成请求堆积)可能导致内存占用过多或OOM
- ArrayBlockintQueue(有界队列):队列长度受限,当队列满了就需要创建多余的线程来执行任务
常见的几种自动创建线程池方式
- 自动创建线程池的几种方式都封装在Executors工具类中:
- newFixedThreadPool:使用的构造方式为new ThreadPoolExecutor(var0, var0, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue()),设置了corePoolSize=maxPoolSize,keepAliveTime=0(此时该参数没作用),无界队列,任务可以无限放入,当请求过多时(任务处理速度跟不上任务提交速度造成请求堆积)可能导致占用过多内存或直接导致OOM异常
- newSingleThreadExector:使用的构造方式为new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue(), var0),基本同newFixedThreadPool,但是将线程数设置为了1,单线程,弊端和newFixedThreadPool一致
- newCachedThreadPool:使用的构造方式为new ThreadPoolExecutor(0, 2147483647, 60L, TimeUnit.SECONDS, new SynchronousQueue()),corePoolSize=0,maxPoolSize为很大的数,同步移交队列,也就是说不维护常驻线程(核心线程),每次来请求直接创建新线程来处理任务,也不使用队列缓冲,会自动回收多余线程,由于将maxPoolSize设置成Integer.MAX_VALUE,当请求很多时就可能创建过多的线程,导致资源耗尽OOM
- newScheduledThreadPool:使用的构造方式为new ThreadPoolExecutor(var1, 2147483647, 0L, TimeUnit.NANOSECONDS, new ScheduledThreadPoolExecutor.DelayedWorkQueue()),支持定时周期性执行,注意一下使用的是延迟队列,弊端同newCachedThreadPool一致
所以根据上面分析我们可以看到,FixedThreadPool和SigleThreadExecutor中之所以用LinkedBlockingQueue无界队列,是因为设置了corePoolSize=maxPoolSize,线程数无法动态扩展,于是就设置了无界阻塞队列来应对不可知的任务量;而CachedThreadPool则使用的是SynchronousQueue同步移交队列,为什么使用这个队列呢?因为CachedThreadPool设置了corePoolSize=0,maxPoolSize=Integer.MAX_VALUE,来一个任务就创建一个线程来执行任务,用不到队列来存储任务;SchduledThreadPool用的是延迟队列DelayedWorkQueue。在实际项目开发中也是推荐使用手动创建线程池的方式,而不用默认方式,关于这点在《阿里巴巴开发规范》中是这样描述的:

handler拒绝策略
- AbortPolicy:中断抛出异常
- DiscardPolicy:默默丢弃任务,不进行任何通知
- DiscardOldestPolicy:丢弃掉在队列中存在时间最久的任务
- CallerRunsPolicy:让提交任务的线程去执行任务(对比前三种比较友好一丢丢)
关闭线程池
- shutdownNow():立即关闭线程池(暴力),正在执行中的及队列中的任务会被中断,同时该方法会返回被中断的队列中的任务列表
- shutdown():平滑关闭线程池,正在执行中的及队列中的任务能执行完成,后续进来的任务会被执行拒绝策略
- isTerminated():当正在执行的任务及对列中的任务全部都执行(清空)完就会返回true
线程池实现线程复用的原理

手动创建线程池(推荐)
那么上面说了使用Executors工具类创建的线程池有隐患,那如何使用才能避免这个隐患呢?对症下药,建立自己的线程工厂类,灵活设置关键参数:
//这里默认拒绝策略为AbortPolicy
private static ExecutorService executor = new ThreadPoolExecutor(10,10,60L, TimeUnit.SECONDS,new ArrayBlockingQueue(10));
使用guava包中的ThreadFactoryBuilder工厂类来构造线程池:
private static ThreadFactory threadFactory = new ThreadFactoryBuilder().build();
private static ExecutorService executorService = new ThreadPoolExecutor(10, 10, 60L, TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(10), threadFactory, new ThreadPoolExecutor.AbortPolicy());
通过guava的ThreadFactory工厂类还可以指定线程组名称,这对于后期定位错误时也是很有帮助的
ThreadFactory threadFactory = new ThreadFactoryBuilder().setNameFormat("thread-pool-d%").build();
3.Springboot中使用线程池
springboot可以说是非常流行了,下面说说如何在springboot中优雅的使用线程池
/**
* @ClassName ThreadPoolConfig
* @Description 配置类中构建线程池实例,方便调用
* @Author simonsfan
* @Date 2018/12/20
* Version 1.0
*/
@Configuration
public class ThreadPoolConfig {
@Bean(value = "threadPoolInstance")
public ExecutorService createThreadPoolInstance() {
//通过guava类库的ThreadFactoryBuilder来实现线程工厂类并设置线程名称
ThreadFactory threadFactory = new ThreadFactoryBuilder().setNameFormat("thread-pool-%d").build();
ExecutorService threadPool = new ThreadPoolExecutor(10, 16, 60L, TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(100), threadFactory, new ThreadPoolExecutor.AbortPolicy());
return threadPool;
}
}
//通过name=threadPoolInstance引用线程池实例
@Resource(name = "threadPoolInstance")
private ExecutorService executorService;
@Override
public void spikeConsumer() {
//TODO
executorService.execute(new Runnable() {
@Override
public void run() {
//TODO
}});
}
4.其它相关
在ThreadPoolExecutor类中有两个比较重要的方法引起了我们的注意:beforeExecute和afterExecute
protected void beforeExecute(Thread var1, Runnable var2) {
}
protected void afterExecute(Runnable var1, Throwable var2) {
}
这两个方法是protected修饰的,很显然是留给开发人员去重写方法体实现自己的业务逻辑,非常适合做钩子函数,在任务run方法的前后增加业务逻辑,比如添加日志、统计等。这个和我们springmvc中拦截器的preHandle和afterCompletion方法很类似,都是对方法进行环绕,类似于spring的AOP,参考下图:

Future和FutureTask(异步任务结果)
Future接口和FutureTask类用来表示执行异步任务的结果,当向ThreadPoolExecutor或ScheduledThreadPoolExecutor提交了一个Callable或Runnable接口的实现类时,ThreadPoolExecutor或ScheduledThreadPoolExecutor就会返回FutureTask。到目前的jdk版本为止,submit返回的是都是实现了Future接口的FutureTask。
<T> Future<T> submit(Callable<T> task);
<T> Future<T> submit(Runnable task, T result);
Future<?> submit(Runnable task);
FutureTask一般都是和线程池搭配使用,用于多线程的方式提交任务,通过futureTask.get()方法获取异步任务的计算结果即可。如下代码:
import java.util.concurrent.*;
public class FutureTaskTest {
public static void main(String[] args) throws ExecutionException, InterruptedException {
ExecutorService executor = Executors.newCachedThreadPool();
try {
CustomCallable callable = new CustomCallable();
FutureTask<Integer> callableTask = new FutureTask<Integer>(callable);
executor.submit(callableTask);
System.out.println("callableTask任务计算结果=" + callableTask.get());
CustomRunnable runnable = new CustomRunnable();
FutureTask<Void> runnableTask = new FutureTask<>(runnable, null);
executor.submit(runnableTask);
} finally {
executor.shutdown();
}
}
}
class CustomCallable implements Callable<Integer> {
@Override
public Integer call() throws Exception {
int sum = 0;
for (int i = 0; i < 10; i++) {
sum += i;
}
return sum;
}
}
class CustomRunnable implements Runnable {
@Override
public void run() {
int sum = 0;
for (int i = 0; i < 10; i++) {
sum += i;
}
System.out.println("RunnableTask任务计算结果=" + sum);
}
}
从上面代码和开头讲到的demo1场景来看,使用FutureTask后,提交给线程池的就变成了futuretask而不是简单的实现了Runnable或Callable接口的普通任务了,并且获取任务的结果也是通过futuretask.get()方法而不是executorservice.submit()返回值。
Callable和Runnable(任务类逻辑)
Callable接口和Runnable接口的实现类(近似等价于被提交任务的逻辑)均可以被ThreadPoolExecutor或ScheduledThreadPoolExecutor所执行,区别在于执行的任务逻辑是否需要返回值,Callable接口实现类可以有返回值,而Runnable接口实现类则没有返回值;比如这里自定义一个实现了Callable接口的任务类:
class CustomCallable implements Callable<Integer> {
@Override
public Integer call() throws Exception {
System.out.println("子线程开始进行计算");
Thread.sleep(500);
int sum = 0;
for (int i = 0; i < 10; i++)
sum += i;
return sum;
}
}
也可以通过Executors类包装的如下两种方式创建任务
//此方式创建Callable对象,通过futureTask.get()方法可以获取到异步计算结果
public static <T> Callable<T> callable(Runnable task, T result) {
if (task == null)
throw new NullPointerException();
return new RunnableAdapter<T>(task, result);
}
//不返回异步计算结果
public static Callable<Object> callable(Runnable task) {
if (task == null)
throw new NullPointerException();
return new RunnableAdapter<Object>(task, null);
}
1、通过futureTask.get()方法获取任务计算结果时,当任务还未完成,会导致线程阻塞直至任务完成,一般会配合futureTask.isDone()方法判断子线程任务是否完成来一起使用;
2、当项目中有很多异步任务时,要着重测试下每个异步任务的执行时间,比如某个异步任务是调用其他系统的web服务,这时候就得测试调用需用的时间长短,如果过长,则建议使用生产/消费模式的消息队列去实现,不然容易使服务器的jvm进程崩溃;
参考资料:慕课网课程《玩转Java并发工具,精通JUC,成为并发多面手》《阿里巴巴Java开发手册v1.2.0》、JDK1.7 java.util.current包
引申阅读
CyclicBarrier(同步屏障)的简单使用
CountDownLatch(闭锁)的简单使用
饭一碗
博客专家
发布了202 篇原创文章 · 获赞 583 · 访问量 151万+ 他的留言板
关注