池化技术
在后端中,对于经常使用池化就是来提升系统性能,比如数据库连接池、线程池连接池等,本质都是利用空间换时间的来提升性能,用来避免资源的频繁创建和销毁,以此提高资源的复用率,所以合理设置系统所需的线程池大小非常重要,一般都需要结合线程启动监控系统来观察,查看设置的是否合理。但是也有缺点,那就是如何无脑的设置,可能会占用过多的内存。所以要避免出现空间过度使用出现内存泄露和频繁垃圾回收的问题。
Java线程池核心原理
不清楚线程池工作原理的,可以看如下文章,从使用到源码解析。
【源码解析】聊聊线程池 实现原理与源码深度解析(一)
【源码解析】聊聊线程池 实现原理与源码深度解析(二)
【Java并发】聊聊线程池原理以及实际应用
【Java并发】聊聊创建线程池的几种方式以及实际生产如何应用
Tomcat自定义线程池
// 自定义的线程队列 最大值是 Integer.MAX_VALUE;
taskqueue = new TaskQueue(maxQueueSize);
// 自定义线程工厂,名称是tomcat-exec- (所以这就是为什么日志中是打印的它)
TaskThreadFactory tf = new TaskThreadFactory(namePrefix,daemon,getThreadPriority());
// 自定义线程池
// 最小线程数 20 最大线程数 200 空闲时间 60S
executor = new ThreadPoolExecutor(getMinSpareThreads(), getMaxThreads(), maxIdleTime, TimeUnit.MILLISECONDS,taskqueue, tf);
可以看到 上面在初始化线程池的时候,创建了一个自定义线程队列以及一个线程工厂。
tomcat自定义线程处理流程
这里对着执行流程进行梳理下,
1.前corePoolSize个任务时,就创建核心线程处理
2.再来任务,就放入任务队列中让所有线程去抢,队列满了,创建临时线程执行。
3.达到最大线程maxNumPoolSize, 继续尝试把任务添加到任务队列中。
4.缓冲队列也满了,插入失败,执行拒绝策略。
我们看具体的code实现,可以发现,先调用java原生线程池执行,超过maximumPoolSize,java原生线程池抛出拒绝策略。尝试放入任务队列中,如果失败,抛出异常。
public void execute(Runnable command, long timeout, TimeUnit unit) {
// 执行一个任务加1
submittedCount.incrementAndGet();
try {
// 调用java原生线程池的execute去执行任务
super.execute(command);
} catch (RejectedExecutionException rx) {
// 如果线程数达到最大 maximumPoolSize Java原生线程池执行拒绝策略
if (super.getQueue() instanceof TaskQueue) {
final TaskQueue queue = (TaskQueue)super.getQueue();
try {
// //继续尝试把任务放到任务队列中去
if (!queue.force(command, timeout, unit)) {
// 执行失败 -1
submittedCount.decrementAndGet();
// //如果缓冲队列也满了,插入失败,执行拒绝策略。
throw new RejectedExecutionException(sm.getString("threadPoolExecutor.queueFull"));
}
} catch (InterruptedException x) {
// // 执行失败 -1
submittedCount.decrementAndGet();
throw new RejectedExecutionException(x);
}
} else {
// // 执行失败 -1
submittedCount.decrementAndGet();
throw rx;
}
}
}
tomcat自定义任务队列
从如下代码中可以看到,自定义了一个任务队列,因为这个队列是一个无界队列,达到核心线程后,就无法创建线程,直接将任务阻塞到队列中。所以通过 submittedCount.incrementAndGet();
submittedCount.decrementAndGet();
记录当前已经提交到线程池,但是还没有执行完的任务个数。
在任务队列的长度无限制的情况下,让线程池有机会创建新的线程。
当然默认情况下Tomcat的任务队列是没有限制的,你可以通过设置maxQueueSize参数来限制任务队列的长度。
public class TaskQueue extends LinkedBlockingQueue<Runnable> {
// 构造方法 调用父类构造方法
public TaskQueue(int capacity) {
super(capacity);
}
@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
//执行到这里,表明当前线程数大于核心线程数,并且小于最大线程数。
//表明是可以创建新线程的,那到底要不要创建呢?分两种情况:
//1. 如果已提交的任务数小于当前线程数,表示还有空闲线程,无需创建新线程
if (parent.getSubmittedCount()<=(parent.getPoolSize())) return super.offer(o);
//if we have less threads than maximum force creation of a new thread
//2. 如果已提交的任务数大于当前线程数,线程不够用了,返回false去创建新线程
if (parent.getPoolSize()<parent.getMaximumPoolSize()) return false;
//if we reached here, we need to add it to the queue
//默认情况下总是把任务添加到任务队列
return super.offer(o);
}
}
小总结
虽然面试中,对于线程池的问题很多,但是如果我们可以结合tomcat自定义线程池的原理来进行复补充,那么不仅可以体现我们对框架内部理解的深度,也可以提升对八股文的应用能力。
tomcat是如何设计的,其实主要就是继承原生ThreadPoolExecutor,重写execute(), 定制自己的任务处理流程。