线程池介绍
线程池是一个专门用来管理和创建线程的工具,因为线程的创建和销毁是基于操作系统的,性能开销比较大,线程池主要是为了避免频繁的对线程创建销毁,提高线程的复用度,解决防止提交任务过多导致资源消耗殆尽这种过分调度问题,保证对内核充分利用。
线程池就类似于一个池塘,池塘里的鱼就是一个一个线程,线程池将线程像鱼一样养到池塘里面,当开发者需要用到线程的时候,直接在池塘里面捞出一条鱼(线程)直接使用。而不用重新等着鱼长大就可以直接使用,使用完成后又扔回池塘,不用管这条鱼的后续处理。
使用线程池的好处:
- 降低资源消耗:能够复用执行完的线程,降低线程创建和销毁代价。
- 提高响应速度:在有线程空闲的时候,能够直接使用线程执行,不必等待线程创建。
- 提高线程管理性:线程池 能够集中管理线程,对线程数量可控,实现线程状态监控,提高系统稳定性。
在Java里面线程池通常就是指的ThreadPoolExecutor,该类实现了线程池基本逻辑,其它定制线程池都是基于该类实现。
线程池参数说明
ThreadPoolExecutor工作主要有7个参数。
corePoolSize:核心线程池大小,当提交一个任务时,线程池会创建一个线程来执行任务,即使其他空的核心线程能够执行新任务也会创建,等待需要执行的任务数大于线程核心大小就不会继续创建。
maximumPoolSize:线程池最大数,⼼许创建的最大线程数,如果队列满了,并且已经创建的线程数小 于最大线程数,则会创建新的线程执行任务。如果是无界队列,这个参数基本没用。
keepAliveTime: 线程保持活动时间,线程池工作线程空闲后,保持存活的时间,所以如果任务很多, 并且每个任务执行时间较短,可以调大时间,提高线程利用率。
unit: 线程保持活动时间单位,天(DAYS)、小时(HOURS)、分钟(MINUTES)、毫秒( MILLISECONDS)、微秒(MICROSECONDS)、纳秒(NANOSECONDS)。
workQueue: 任务队列,保存等待执行的任务的不塞队列。 一般来说可以选择如下:
- ArrayBlockingQueue:基于数组的有界阻塞队列。
- LinkedBlockingQueue:基于链表的阻塞队列。
- SynchronousQueue:一个不存储元素的阻塞队列,只是一个单纯的传递队列。
- PriorityBlockingQueue:一个具有优先级的阻塞队列。
threadFactory:设置创建线程的工厂,可以通过线程工厂给每个创建出来的线程设置更有意义的名字。
handler:饱和策略也叫拒绝策略。当队列和线程池都满了,即达到饱和状态。所以需要采取策略来处理新的任务。默认策略是AbortPolicy。
- AbortPolicy:直接抛出异常。
- CallerRunsPolicy: 调用者所在的线程来运行任务。
- DiscardOldestPolicy:丢弃队列里最近的一个任务,并执行当前任务。
- DiscardPolicy:不处理,直接丢掉。当然可以根据自己的应用场景,实现RejectedExecutionHandler接口自定义策略。
线程池工作流程
当有新任务来的时候会,并且当前线程数小于corePoolSize后,会新建线程来处理当前的新任务。如果当前线程池中的线程数已经达到了corePoolSize的线程数的时候会将新来的任务线程放入workQueue线程队列里面,然后等待空闲线程前来执行。如果当队列满了的时候还没有空闲线程来消费队列里面的任务,则会判断当前线程池中的线程数是否小于maximumPoolSize最大线程数,如果小于则继续创建线程执行队列的任务,这个时候创建的线程不会被回收到线程池里面。如果线程队列满了,并且活动线程达到了maximumPoolSize线程数,则这个时候就会执行拒绝策略,一般Executor里面的默认四个线程池的拒绝策略都是报错,我们一般自定义线程池的时候一般会自定义拒绝策略。
线程池使用
JDK自带四种线程池
Java通过Executors(jdk1.5并发包)提供四种线程池,分别为:
- newCachedThreadPool 创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
- newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
- newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行。
- newSingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
CachedThreadPool
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
CachedThreadPool是一个极具伸缩性的线程池,理论上可以无限制的提交任务,使用的同步队列SynchronousQueue。该线程池会在任务提交过程中,不停的创建线程来执行任务,并且同步队列SynchronousQueue是一个不存储任务的队列,执行完成后也不会回收线程,会直接销毁,因此有耗尽CPU的风险,适用于高频度的任务提交中,有一定风险谨慎使用。
FixedThreadPool
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
定长线程池,该线程池会一直保持指定数量的线程,在超过了线程处理能力的任务,会放在阻塞队列LinkedBlockingQueue中,该队列是一个无界阻塞队列,因此理论上来说是可以无限制的存放提交任务,因此又内存耗尽的风险,适用于预先估算得出任务大小的情况下使用。
SingleThreadExecutor
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
单一线程池,该线程池始终维护一个活动线程,并且使用LinkedBlockingQueue作为任务存储队列,和定长线程池有相同的问题有内存耗尽风险,适用于串行任务执行。
ScheduledThreadPoolExecutor
public ScheduledThreadPoolExecutor(int corePoolSize) {
super(corePoolSize, Integer.MAX_VALUE,
DEFAULT_KEEPALIVE_MILLIS, MILLISECONDS,
new DelayedWorkQueue());
}
计划线程池,该线程池用于执行定时任务和周期任务,内部主要采用了一个DelayedWorkQueue延时队列,该队列内部是一个堆的数据结构,会将周期靠前的任务排在队列前面,然后ScheduledThreadPoolExecutor会从该队列中取出最前面的任务执行。
Timer也能够定时周期的执行任务,但是对比ScheduledThreadPoolExecutor,ScheduledThreadPoolExecutor功能更加强大,更加稳定。首先ScheduledThreadPoolExecutor支持多线程执行任务,并且ScheduledThreadPoolExecutor在执行多任务的时候如果前面的任务出错导致中断了,不会影响后续任务,ScheduledThreadPoolExecutor支持Future能够获取执行结果。ScheduledThreadPoolExecutor调度是基于相对时间调度,不会受到系统时间影响。
在日常开发中需要用到定时、周期任务的地方,使用ScheduledThreadPoolExecutor不再使用Timer
线程池的选择
JDK自带的四种线程池能够满足一定场景需要,但是JDK线程池在不了解其底层原理和工作流程的情况下都有资源耗尽的风险,因此我们通常在系统中需要自定义线程池参数,这样的目的是为了让定义线程池的人能够明确运行规则,选用合适的参数,防止资源耗尽。并且经常还需要自定义线程池的拒绝策略和工厂,方便出现异常的时候能够有效处理
线程池的使用
自定义线程工厂
线程池的第6个参数threadFactory支持传递一个线程工厂,线程池中每个线程都基于该工厂创建。这样做的好处可以接管线程的创建,为线程赋值,或者指定有意义的命名。在我的工作中,我们就有局部线程池这样干,每个任务都会通过该工厂进行一次包装,并指定名称,在任务提交给当前线程池执行的时候,执行增强了线程对象,出现错误的时候也能即时看见是哪个任务出现了错误。
示例:
现在我们有一个执行任务分发的线程池,该服务可能会部署到各个物理网络隔离的节点,要求出现错误的时候错误信息能够打印出有意义的业务含义,最后汇总的时候方便我们分析是哪一个单位的任务分发没有成功。
public static void main(String[] args) {
MessageTaskThreadFactory taskThreadFactory = new MessageTaskThreadFactory();
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
1,
2,
60,
TimeUnit.SECONDS, new LinkedBlockingQueue<>(100),
taskThreadFactory
);
for (int i = 0; i < 100; i++) {
threadPoolExecutor.execute(new Task());
}
}
public static class Task implements Runnable {
private static AtomicInteger ai = new AtomicInteger();
@Override
public void run() {
String name = Thread.currentThread().getName();
int seq = ai.incrementAndGet();
System.out.println(name + " " + " 重要文件[" + seq + "],开始发送......");
if (seq == 4)
throw new RuntimeException("网络错误");
System.out.println(name + " " + " 重要文件[" + seq + "],发送成功......");
}
}
// 自定义线程工厂
public static class MessageTaskThreadFactory implements ThreadFactory {
@Override
public Thread newThread(Runnable r) {
String orgName = readConfigOrg();
Thread thread = new Thread(r, orgName);
return thread;
}
// 读取在部署的时候配置的单位名称
private String readConfigOrg() {
return "单位A";
}
}
结果:
当出现了错误,由实施部门发送来的错误工单,能够直接定位到是哪个单位,出现了什么问题,能够方便的排查错误。
如果没有自定义的线程工厂
可以看到,这里的错误信息并不明确,并不知道是哪个地方的单位出现了问题。
ps:以上只是一个简单的应用,在我们实际中,我们扩展自定义线程Thread,并对一些个性化节点配置信息,如网络节点选择、分发策略选择等逻辑也放在这里面,让任务Runnable只关注发送逻辑,不用管每个单位的个性化节点配置。
自定义拒绝策略
线程工厂做的是线程创建前的工作,对初始化线程、明确线程执行情况很有帮助。在我们实际重要业务中,除了需要明确业务线程的执行情况,还得充分考虑任务突然增长返回相应的提示或者友好跳转,也就是微服务里面的降级概念。
JDK提供的拒绝策略过于简单粗暴,在我们的业务场景中需要对触发了拒绝策略的任务进行记录,记录触发次数,将被拒绝的任务放入MQ进入排队队列逻辑。
public static class MessageTaskRejectedExecutionHandler implements RejectedExecutionHandler {
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
Task task = (Task) r;
mq_queue.offer(task);// 放入mq
System.out.println("当前节点处理任务已满,您提交的任务将尝试分发到其它节点执行....");
}
}
处理异常手段
拒绝策略主要是为了处理超过线程池处理能力的任务属于是预期的中的问题,但是任务执行过程中经常会遇到不可控的异常,比如网络连接失败,参数错误等。出现了这种错误往往需要告警,人工干预。因此需要合理的定义异常处理机制,因为一些特殊应用的局部线程池里面,执行的任务都是同类型的,因此可以统一处理异常。
在线程池中,也是具备回调函数的,有beforeExecute afterExecute分别对应了任务执行之前和之后
ps:通常我们可以用这两个方法来统计任务执行时间,在beforeExecute中记录任务开始时间,在afterExecute记录任务结束时间,可以来计算任务执行时间,计算平均时长,分析相关业务性能。
通常我们统一处理异常就是在afterExecute方法里面,方法如下
@Override
protected void afterExecute(Runnable r, Throwable t) {}
第一个参数为我们提交的任务,第二参数为执行过程中是否抛出了异常,如果t为null则说明执行过程中没有出现异常。
这两个参数跟线程池,提交任务的方式有关,线程池执行任务可以使用submit和execute两个方法执行,submit会返回一个Future,execute没有返回值。
如果是使用execute执行任务,则在afterExecute获取的Runnable r就是提交的任务,并且出现异常也是能够直接获取的
MessageTaskThreadFactory taskThreadFactory = new MessageTaskThreadFactory();
MessageTaskRejectedExecutionHandler rejectedExecutionHandler = new MessageTaskRejectedExecutionHandler();
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(1,2,60,TimeUnit.SECONDS, new LinkedBlockingQueue<>(2),
taskThreadFactory,
rejectedExecutionHandler
){
@Override
protected void afterExecute(Runnable r, Throwable t) {
System.out.println("afterExecute..."+r);
System.out.println("异常:" + t);
}
};
Task task = new Task();
System.out.println("create..."+task);
threadPoolExecutor.execute(task);
// 结果:
// create..Task@26ba2a48
// afterExecute...Task@26ba2a48
// 异常:java.lang.RuntimeException: 网络异常
如果是使用submit执行任务,则在afterExecute获取的Runnable r是被包装过的FutureTask,并且异常也是不能直接拿到的,因为FutureTask任务,必须调用了get方法后才能,获取到具体执行结果。
MessageTaskThreadFactory taskThreadFactory = new MessageTaskThreadFactory();
MessageTaskRejectedExecutionHandler rejectedExecutionHandler = new MessageTaskRejectedExecutionHandler();
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
1,
2,
60,
TimeUnit.SECONDS, new LinkedBlockingQueue<>(2),
taskThreadFactory,
rejectedExecutionHandler
) {
@Override
protected void afterExecute(Runnable r, Throwable t) {
System.out.println("afterExecute..." + r);
System.out.println("异常:" + t);
}
};
Task task = new Task();
System.out.println("create..." + task);
threadPoolExecutor.submit(task);
// 结果:
// create...Task@3b81a1bc
// afterExecute...java.util.concurrent.FutureTask@6094727d
// 异常:null
想要获取到submit执行过程的异常,必须要调用FutureTask的get方法,如下:
protected void afterExecute(Runnable r, Throwable t) {
if (r instanceof FutureTask) {
FutureTask ft = (FutureTask) r;
try {
Object o = ft.get();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
因此如果要判断处理任务执行过程中的异常的时候,一定要明确任务是通过execute提交的还是submit提交的,在我们的业务中直接就是execute,然后捕获到了未知异常,直接告警。
线程池终止
线程池提供了两种终止方式,分别是shutdown()和shutdownNow()方法:
shutdown:将线程池的状态修改为SHUTDOWN,并且将线程池中的空闲线程中断,不再接收新提交的任务。正在执行的任务和队列的任务不做处理,等待执行完毕。
shutdownNow:将线程池的状态修改为STOP,并且尝试将线程池中的所有线程都中断,不再接收新提交的任务,并且返回正在等待的任务列表。(这里的中断只是置为interrupt,需要线程响应中断)。
因为线程池终止了以后不能再使用,因此在一些业务场景中,只是需要拒绝任务提交,而不是直接终止线程池,应该叫做关闭线程池。在我们的业务场景中,会有时间限制,在一定时间段内才能提交任务,因此会单独提供方法出来,关闭线程池,禁止任务提交。
动态线程池
线程池在运行过程中,提交的任务会根据业务增长或者衰减,因此一开始配置的线程池参数不一定能够满足后期的的需要,线程池提供在运行中set的get方法,因此可以基于这点,动态监控线程池状态,便于动态调整线程池参数。