Tomcat如何打破双亲委托机制
什么是双亲委托机制
Java 的类加载,就是把字节码格式 ".class" 文件加载到 JVM 的方法区,并在 JVM 的堆区建立一个 java.lang.Class 对象的实例,用来封装 Java 类相关的数据和方法。
JVM 类加载是由类加载器来完成的,JDK 提供了一个抽象类 ClassLoader。
public abstract class ClassLoader {
//每个类加载器都有个父加载器
private final ClassLoader parent;
public Class<?> loadClass(String name) {
//查找一下这个类是不是已经加载过了
Class<?> c = findLoadedClass(name);
//如果没有加载过
if( c == null ){
//先委托给父加载器去加载,注意这是个递归调用
if (parent != null) {
c = parent.loadClass(name);
}else {
// 如果父加载器为空,查找Bootstrap加载器是不是加载过了
c = findBootstrapClassOrNull(name);
}
}
// 如果父加载器没加载成功,调用自己的findClass去加载
if (c == null) {
c = findClass(name);
}
return c;
}
protected Class<?> findClass(String name){
//1. 根据传入的类名name,到在特定目录下去寻找类文件,把.class文件读入内存
...
//2. 调用defineClass将字节数组转成Class对象
return defineClass(buf, off, len);
}
// 将字节码数组解析成一个Class对象,用native方法实现
protected final Class<?> defineClass(byte[] b, int off, int len){
...
}
}
根据上述代码,可以知道加载类的过程如下:
首先检查类是否被加载过,如果加载过了直接返回,否则交给父加载器去加载,当父加载器在自己的加载范围内找不到时,才会交给子加载器加载。这个过程就是双亲委托机制。这个机制保证了一个类在 JVM 中是唯一的。
**
JDK 的类加载器
- BootstrapClassLoader 是启动类加载器,由 C 语言实现,用来加载 JVM 启动时所需要的核心类,比如 rt.jar、resources.jar 等
- ExtClassLoader 是扩展类加载器,用来加载 jre/lib/ext 目录下 JAR 包
- AppClassLoader 是系统类加载器,用来加载 classpath 下的类,应用程序默认用它来加载类
- 自定义类加载器,用来加载自定义路径下的类
tomcat 如何打破双亲委托机制
通过自定义一个类加载器 WebappClassLoader 来完成这项工作。
WebappClassLoader 继承自 ClassLoader。
其加载类的逻辑:
- 先查看自己(指 WebappClassLoader)有没有加载过该类,有则返回
- 从系统加载器中查找是否加载过,有则返回
- 尝试使用 ExtClassLoader 类加载器加载,有则返回
- 从本地目录加载
- 尝试使用系统类加载器加载(即 AppClassLoader)
在进行本地目录加载前,先用了 ExtClassLoader 加载是为了避免覆盖掉 jre/lib/ext 下的类以及核心类
WebappClassLoader 的定制化加载逻辑是为了实现:优先加载 web 应用目录下的类,然后再加载其他目录下的类。
Tomcat如何定制化线程池
JDK 线程池
为了区分 JDK 原生线程池和 Tomcat 定制化后的线程池之间的差异,下面先简单描述下 JDK 线程池的工作原理。
线程池的使用例子
ThreadPoolExecutor executor = new ThreadPoolExecutor(10, 20, 60,
TimeUnit.SECONDS, new LinkedBlockingQueue<>());
executor.execute(() -> {
System.out.println("线程池完成的任务");
});
提交任务的流程
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
int c = ctl.get();
if (workerCountOf(c) < corePoolSize) {
// 如果核心线程未全部启动启动核心线程,执行本次提交任务
if (addWorker(command, true))
return;
c = ctl.get();
}
// 核心线程已占满
// 线程池未关闭,则将本次任务添加到任务队列
if (isRunning(c) && workQueue.offer(command)) {
// 双重校验,如果线程池关闭了,那么将本次任务移出任务队列。
int recheck = ctl.get();
if (! isRunning(recheck) && remove(command))
// 成功移出任务队列,执行决绝策略
reject(command);
else if (workerCountOf(recheck) == 0)
// 如果没有可运行线程,启动一个非核心线程执行本次任务
addWorker(null, false);
}
// 无法添加到任务队列,那么启动非核心线程
else if (!addWorker(command, false))
// 线程数已满,执行决绝策略
reject(command);
}
线程池的执行行为由核心线程数、最大线程数、任务队列三者控制。
- 优先启动核心线程数大小的线程;
- 如果达到核心线程数,则将任务放入任务队列。
- 任务队列容量满时,启动新线程,直到线程数达到最大线程数。
Tomcat对线程池的改造
线程池默认情况下,不会启动最大线程数线程,而是将提交过来的任务放入任务队列。为了最大压榨服务器的性能,Tomcat 将优先启动最大线程数指定的线程用于处理请求。具体的优化手段是通过传入一个定制化的队列:TaskQueue 。
public class TaskQueue extends LinkedBlockingQueue<Runnable> {
// 省略一些代码
@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);
//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);
}
}
public void execute(Runnable command, long timeout, TimeUnit unit) {
// 记录提交任务数
submittedCount.incrementAndGet();
try {
// 执行默认提交任务的逻辑
super.execute(command);
} catch (RejectedExecutionException rx) {
// 发生拒绝策略时,如果为定制化任务队列
if (super.getQueue() instanceof TaskQueue) {
final TaskQueue queue = (TaskQueue)super.getQueue();
try {
// 带超时机制的方式,将任务放入队列
if (!queue.force(command, timeout, unit)) {
submittedCount.decrementAndGet();
throw new RejectedExecutionException(sm.getString("threadPoolExecutor.queueFull"));
}
} catch (InterruptedException x) {
submittedCount.decrementAndGet();
throw new RejectedExecutionException(x);
}
} else {
submittedCount.decrementAndGet();
throw rx;
}
}
}
通过以上改造,Tomcat 优先启动最大线程数来执行任务,并且在发生拒绝策略时,利用带超时机制的入队策略,将任务放入队列中。