Tomcat定制化类加载器和线程池的原理

1,227 阅读4分钟

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);
}

线程池的执行行为由核心线程数、最大线程数、任务队列三者控制。

  1. 优先启动核心线程数大小的线程;
  2. 如果达到核心线程数,则将任务放入任务队列。
  3. 任务队列容量满时,启动新线程,直到线程数达到最大线程数。

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 优先启动最大线程数来执行任务,并且在发生拒绝策略时,利用带超时机制的入队策略,将任务放入队列中。