线程池的设置
- corePoolSize:核心线程数
- queueCapacity:任务队列容量(阻塞队列)
- maxPoolSize:最大线程数
- keepAliveTime:线程空闲时间
- allowCoreThreadTimeout:允许核心线程超时 默认
- rejectedExecutionHandler:任务拒绝处理器
- ThreadPoolExecutor执行顺序
- 使用分析
- 举例
- 实战应用
- 需求
- 防止重复
- 失败机制
- 线程池选择
- 核心代码
线程池的设置
corePoolSize:核心线程数
核心线程会一直存活,即使没有任务需要执行
当线程数小于核心线程数时,即使有线程空闲,线程池也会优先创建新线程处理
设置allowCoreThreadTimeout=true(默认false)时,核心线程会超时关闭
(1)高并发、任务执行时间短的业务,线程池线程数可以设置为CPU核数+1,减少线程上下文的切换
(2)并发不高、任务执行时间长的业务要区分开看:
a)假如是业务时间长集中在IO操作上,也就是IO密集型的任务,因为IO操作并不占用CPU,所以不要让所有的CPU闲下来,可以加大线程池中的线程数目,让CPU处理更多的业务
b)假如是业务时间长集中在计算操作上,也就是计算密集型任务,这个就没办法了,和(1)一样吧,线程池中的线程数设置得少一些,减少线程上下文的切换
(3)并发高、业务执行时间长,解决这种类型任务的关键不在于线程池而在于整体架构的设计,看看这些业务里面某些数据是否能做缓存是第一步,增加服务器是第二步,至于线程池的设置,设置参考(2)。最后,业务执行时间长的问题,也可能需要分析一下,看看能不能使用中间件对任务进行拆分和解耦
tasks :每秒的任务数,假设为500~1000
taskcost:每个任务花费时间,假设为0.1s
responsetime:系统允许容忍的最大响应时间,假设为1s
做几个计算
corePoolSize = 每秒需要多少个线程处理?
threadcount = tasks/(1/taskcost) =tasks*taskcout = (500~1000)*0.1 = 50~100 个线程。corePoolSize设置应该大于50
根据8020原则,如果80%的每秒任务数小于800,那么corePoolSize设置为80即可
queueCapacity:任务队列容量(阻塞队列)
最常使用的两种队列
ArrayBlockingQueue
是一个基于数组结构的有界阻塞队列,底层结构是一个数组
LinkedBlockingQueue
一个基于链表结构的有界阻塞队列(不设置大小时,默认为Integer.MAX_VALUE),底层结构是一个单向链表
当核心线程数达到最大时,新任务会放在队列中排队等待执行,如果长度设置的不合理就无法发挥出多线程的威力。队列默认长度是int最大值,
队列长度设置此长度,那么线程池个数将只会增加到corePoolSize
,如果corePoolSize个数设置又过小,这样就会无法发挥出多线程的威力。
Tomcat的线程池队列是无限长度的,但是线程池会一直创建到maximumPoolSize,然后才把请求放入等待队列中
tomcat 任务队列
org.apache.tomcat.util.threads.TaskQueue其继承与LinkedBlockingQueue,覆写offer方法。
@Override
public boolean offer(Runnable o) {
//we can't do any checks
if (parent==null) return super.offer(o);
//we are maxed out on threads, simply queue the object
if (parent.getPoolSize() == parent.getMaximumPoolSize()) return super.offer(o);
//we have idle threads, just add it to the queue
if (parent.getSubmittedCount()<(parent.getPoolSize())) return super.offer(o);
//线程个数小于MaximumPoolSize会创建新的线程。
//if we have less threads than maximum force creation of a new thread
if (parent.getPoolSize()<parent.getMaximumPoolSize()) return false;
//if we reached here, we need to add it to the queue
return super.offer(o);
}
queueCapacity = (coreSizePool/taskcost)responsetime
计算可得 queueCapacity = 80/0.11 = 80。意思是队列里的线程可以等待1s,超过了的需要新开线程来执行
切记不能设置为Integer.MAX_VALUE,这样队列会很大,线程数只会保持在corePoolSize大小,当任务陡增时,不能新开线程来执行,响应时间会随之陡增。
maxPoolSize:最大线程数
当线程数>=corePoolSize,且任务队列已满时。线程池会创建新线程来处理任务
当线程数=maxPoolSize,且任务队列已满时,线程池会拒绝处理任务而抛出异常
maxPoolSize = (max(tasks)- queueCapacity)/(1/taskcost)(最大任务数-队列容量)/每个线程每秒处理能力 = 最大线程数
计算可得 maxPoolSize = (1000-80)/10 = 92
rejectedExecutionHandler:根据具体情况来决定,任务不重要可丢弃,任务重要则要利用一些缓冲机制来处理
keepAliveTime和allowCoreThreadTimeout:采用默认通常能满足
keepAliveTime:线程空闲时间
当线程空闲时间达到keepAliveTime时,线程会退出,直到线程数量=corePoolSize 默认
allowCoreThreadTimeout:允许核心线程超时 默认
如果allowCoreThreadTimeout=true,则会直到线程数量=0
rejectedExecutionHandler:任务拒绝处理器
根据具体情况来决定,任务不重要可丢弃,任务重要则要利用一些缓冲机制来处理
两种情况会拒绝处理任务:
1):当线程数已经达到maxPoolSize,且队列已满,会拒绝新任务;
2):当线程池调用shutdown()后,会等待队列里的任务执行完毕,再关闭。如果在调用shutdown()和线程池真正关闭之间提交任务,会拒绝新任务;
线程池会调用rejectedExecutionHandler来处理这个任务。如果没有设置默认是 AbortPolicy,会抛出异常
线程池提供的拒绝策略:
ThreadPoolExecutor.AbortPolicy:丢弃任务,抛运行时异常
public static class AbortPolicy implements RejectedExecutionHandler {
public AbortPolicy() { }
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
throw new RejectedExecutionException("Task " + r.toString() +
" rejected from " +
e.toString());
}
}
ThreadPoolExecutor.CallerRunsPolicy:将任务分给调用线程来执行,运行当前被丢弃的任务,这样做不会真的丢弃任务,但是提交的线程性能有可能急剧下降
public static class CallerRunsPolicy implements RejectedExecutionHandler {
public CallerRunsPolicy() { }
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
if (!e.isShutdown()) {
r.run();
}
}
}
ThreadPoolExecutor.DiscardPolicy:忽视,什么都不会发生
public static class DiscardPolicy implements RejectedExecutionHandler {
public DiscardPolicy() { }
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
}
}
ThreadPoolExecutor.DiscardOldestPolicy:从队列中踢出最先进入队列(最后一个执行)的任务
public static class DiscardOldestPolicy implements RejectedExecutionHandler {
public DiscardOldestPolicy() { }
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
if (!e.isShutdown()) {
e.getQueue().poll();
e.execute(r);
}
}
}
三种策略都是会丢弃原有的任务。但是在某些业务场景下,我们不能够粗暴的丢弃任务。另一种拒绝策略,是通过启动线程池的线程来处理丢弃的任务,但是问题是即便是线程池空闲,它也不会执行丢弃的任务,而是等待调用线程池的主线程来执行任务,直到任务结束。
实现RejectedExecutionHandler接口,可自定义处理器
在线程池的定义中我们可以看到拒绝策略有个统一的实现接口,如下:
public interface RejectedExecutionHandler {
void rejectedExecution(Runnable r, ThreadPoolExecutor executor);
}
我们可以根据自己的业务需求来定义符合自己业务场景的处理策略。
1、Netty 中的线程池拒绝策略
private static final class NewThreadRunsPolicy implements RejectedExecutionHandler {
NewThreadRunsPolicy() {
super();
}
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
try {
final Thread t = new Thread(r, "Temporary task executor");
t.start();
} catch (Throwable e) {
throw new RejectedExecutionException(
"Failed to start a new thread", e);
}
}
}
Netty的处理方式就是不丢弃任务,这个思想和CallerRunsPolicy优点类似。只是在Netty框架中的自定义拒绝策略中,是通过新建工作线程来完成被丢弃的任务的,但是我们看一看得出它在创建线程时,没有进行条件约束,只要资源允许就不断创建新的线程来进行处理。
Dubbo 中的线程池拒绝策略
public class AbortPolicyWithReport extends ThreadPoolExecutor.AbortPolicy {
protected static final Logger logger = LoggerFactory.getLogger(AbortPolicyWithReport.class);
private final String threadName;
private final URL url;
private static volatile long lastPrintTime = 0;
private static Semaphore guard = new Semaphore(1);
public AbortPolicyWithReport(String threadName, URL url) {
this.threadName = threadName;
this.url = url;
}
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
String msg = String.format("Thread pool is EXHAUSTED!" +
" Thread Name: %s, Pool Size: %d (active: %d, core: %d, max: %d, largest: %d), Task: %d (completed: %d)," +
" Executor status:(isShutdown:%s, isTerminated:%s, isTerminating:%s), in %s://%s:%d!",
threadName, e.getPoolSize(), e.getActiveCount(), e.getCorePoolSize(), e.getMaximumPoolSize(), e.getLargestPoolSize(),
e.getTaskCount(), e.getCompletedTaskCount(), e.isShutdown(), e.isTerminated(), e.isTerminating(),
url.getProtocol(), url.getIp(), url.getPort());
logger.warn(msg);
dumpJStack();
throw new RejectedExecutionException(msg);
}
private void dumpJStack() {
//省略实现
}
}
Dubbo中的自定义拒绝策略中,打印了日志,输出当前线程的堆栈信息以及执行JDK的默认拒绝策略。
自定义
public class CustomRejectionHandler implements RejectedExecutionHandler {
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
// 打印日志、暂存任务、重新执行等拒绝策略
}
}
/**
* 自定义拒绝策略
*/
publice class CustomRejectedExecutionHandler implements RejectedExecutionHandler {
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
try {
// 核心改造点,由blockingqueue的offer改成put阻塞方法
executor.getQueue().put(r);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
如果任务量很大,还要求每个任务都处理成功,要对提交的任务进行阻塞提交,重写拒绝机制,改为阻塞提交。保证不抛弃一个任务
ThreadPoolExecutor执行顺序
线程池按以下行为执行任务
(1)当线程数小于核心线程数时,创建线程。
(2)当线程数大于等于核心线程数,且任务队列未满时,将任务放入任务队列。
(3)当线程数大于等于核心线程数,且任务队列已满
a)若线程数小于最大线程数,创建线程
b)若线程数等于最大线程数,抛出异常,拒绝任务
使用分析
针对快速响应用户请求的业务场景,我们应该从用户体验角度看,这个结果响应的越快越好,如果一个页面半天都刷不出,用户可能就放弃查看这个商品了。而面向用户的功能聚合通常非常复杂,伴随着调用与调用之间的级联、多级级联等情况,业务开发同学往往会选择使用线程池这种简单的方式,将调用封装成任务并行的执行,缩短总体响应时间。另外,使用线程池也是有考量的,这种场景最重要的就是获取最大的响应速度去满足用户,所以应该不设置队列去缓冲并发任务,调高corePoolSize和maxPoolSize去尽可能创造多的线程快速执行任务。
至于快速处理批量任务,这种场景需要执行大量的任务,我们也会希望任务执行的越快越好。这种情况下,也应该使用多线程策略,并行计算。但与响应速度优先的场景区别在于,这类场景任务量巨大,并不需要瞬时的完成,而是关注如何使用有限的资源,尽可能在单位时间内处理更多的任务,也就是吞吐量优先的问题。所以应该设置队列去缓冲并发任务,调整合适的corePoolSize去设置处理任务的线程数。在这里,设置的线程数过多可能还会引发线程上下文切换频繁的问题,也会降低处理任务的速度,降低吞吐量。
举例
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import java.util.concurrent.*;
public class ThreadTask {
public static void testHospTest() {
// 创建一个名为shopTest-pool-%d的线程池
ThreadFactory threadFactory = new ThreadFactoryBuilder().setNameFormat("shopTest-pool-%d").build();
/** 构建线程池参数
* 1.corePoolSize 核心线程数量
* 2.maximumPoolSize 能创建的最大线程数,最大线程数不能大于核心线程数
* 3.keepAliveTime 也就是当线程空闲时,所允许保存的最大时间,超过这个时间,线程将被释放销毁,但只针对于非核心线程
* 4.TimeUnit 时间单位,TimeUnit.MICROSECONDS等
* 5.workQueue 工作队列,这里有几种
* 5.1 ArrayBlockingQueue 基于数组的有界阻塞队列,必须设置容量,遵循先进先出原则(FIFO)对元素进行排序。
* 5.2 LinkedBlockingQueue:一个基于链表结构的阻塞队列,可以设置容量,此队列按FIFO (先进先出) 排序元素,吞吐量通常要高于ArrayBlockingQueue
* 5.3 SynchronousQueue:一个不存储元素的阻塞队列。每个插入offer操作必须等到另一个线程调用移除poll操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQueue。
* 5.4 PriorityBlockingQueue:一个具有优先级的无限阻塞队列。
* 6.threadFactory 线程工厂,用于创建线程
* 7.handler 当线程边达到最大容量时,用于处理阻塞时的程序策略
* 7.1
ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。
* 7.2
ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,但是不抛出异常。
* 7.3
ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程)
* 7.4
ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务
* 8.executorService.execute 执行一个实现Runnable 接口的线程
*
9.executorService.shutdown();停止线程池
*/
// 构建线程参数
ExecutorService executorService = new ThreadPoolExecutor(3,
3, 0l,
TimeUnit.MICROSECONDS,
new LinkedBlockingQueue<>(3), threadFactory, new ThreadPoolExecutor.CallerRunsPolicy());
try {
int a = 20;
// 执行线程
executorService.execute(new Seller(a));
} catch (Exception e) {
System.err.println("数值小于0"+e.getMessage());
} finally {
// 停止线程池
executorService.shutdown();
}
}
}
public class Seller implements Runnable {
private int ticket;
public Seller(int ticket) {
this.ticket = ticket;
}
@Override
public void run() {
if (ticket > 0) {
while (ticket > 0) {
ticket--;
System.out.println("你已经白嫖了" + ticket + "次");
}
} else {
System.err.println("输入参数有误");
}
}
}
实战应用
需求
我们需要推送给医疗的数据大概每月月初三天内三千万条数据,但是第三方监管提供的接口只支持3000条数据推送可以估算一下,三千万条数据,一个3000条数据按3秒算,大概需要25小时左右。推送万数据后,还要核实数据,处理失败数据,
所以就考虑到引入多线程来进行并发操作,降低数据推送的时间,提高数据推送的实时性。
防止重复
我们推送给第三方的数据肯定是不能重复推送的,必须要有一个机制保证各个线程推送数据的隔离。
利用 数据库分页的方式,每个线程取 [start,limit] 区间的数据推送,我们需要保证start的一致性
失败机制
我们还得考虑到线程推送数据失败的情况。
如果是自己的系统,我们可以把多线程调用的方法抽出来加一个事务,一个线程异常,整体回滚。
但是是和第三方的对接,我们都没法做事务的,所以,我们采用了直接在数据库记录失败状态的方法,可以在后面用其它方式处理失败的数据。
线程池选择
在实际使用中,我们肯定是要用到线程池来管理线程,关于线程池,我们常用 ThreadPoolExecutor提供的线程池服务,SpringBoot中同样也提供了线程池异步的方式,虽然SprignBoot异步可能更方便一点,但是使用ThreadPoolExecutor更加直观地控制线程池,所以我们直接使用ThreadPoolExecutor构造方法创建线程池。
核心代码
@Service
public class PushProcessServiceImpl implements PushProcessService
{
@Autowired
private PushUtil pushUtil;
@Autowired
private PushProcessMapper pushProcessMapper;
private final static Logger logger = LoggerFactory.getLogger(PushProcessServiceImpl.class);
//每个线程每次查询的条数
private static final Integer LIMIT = 300000;
//起的线程数
private static final Integer THREAD_NUM = 5;
//创建线程池
ThreadPoolExecutor pool = new ThreadPoolExecutor(THREAD_NUM, THREAD_NUM * 2, 0, TimeUnit.SECONDS, new LinkedBlockingQueue<>(100));
@Override
public void pushData() throws ExecutionException, InterruptedException {
//计数器,需要保证线程安全
int count = 0;
//未推送数据总数
Integer total = pushProcessMapper.countPushRecordsByState(0);
logger.info("未推送数据条数:{}", total);
//计算需要多少轮
int num = total / (LIMIT * THREAD_NUM) + 1;
logger.info("要经过的轮数:{}", num);
//统计总共推送成功的数据条数
int totalSuccessCount = 0;
for (int i = 0; i < num; i++) {
//接收线程返回结果
List<Future<Integer>> futureList = new ArrayList<>(32);
//起THREAD_NUM个线程并行查询更新库,加锁
for (int j = 0; j < THREAD_NUM; j++) {
synchronized (PushProcessServiceImpl.class) {
int start = count * LIMIT;
count++;
//提交线程,用数据起始位置标识线程
Future<Integer> future = pool.submit(new PushDataTask(start, LIMIT, start));
//先不取值,防止阻塞,放进集合
futureList.add(future);
}
}
//统计本轮推送成功数据
for (Future f : futureList) {
totalSuccessCount = totalSuccessCount + (int) f.get();
}
}
//更新推送标志
pushProcessMapper.updateAllState(1);
logger.info("推送数据完成,需推送数据:{},推送成功:{}", total, totalSuccessCount);
}
/**
* 推送数据线程类
*/
class PushDataTask implements Callable<Integer> {
int start;
int limit;
int threadNo; //线程编号
PushDataTask(int start, int limit, int threadNo) {
this.start = start;
this.limit = limit;
this.threadNo = threadNo;
}
@Override
public Integer call() throws Exception {
int count = 0;
//推送的数据
List<PushProcess> pushProcessList = pushProcessMapper.findPushRecordsByStateLimit(0, start, limit);
if (CollectionUtils.isEmpty(pushProcessList)) {
return count;
}
logger.info("线程{}开始推送数据", threadNo);
for (PushProcess process : pushProcessList) {
boolean isSuccess = pushUtil.sendRecord(process);
if (isSuccess) { //推送成功
//更新推送标识
pushProcessMapper.updateFlagById(process.getId(), 1);
count++;
} else { //推送失败
pushProcessMapper.updateFlagById(process.getId(), 2);
}
}
logger.info("线程{}推送成功{}条", threadNo, count);
return count;
}
}
}