【基础】ThreadPoolExecutor

7大核心参数
  1. corePoolSize
  2. maximumPoolSize
  3. keepAliveTime
  4. unit
  5. workQueue
  6. threadFactory
  7. RejectedExecutionHandler

线程池执行流程图

4种拒绝策略
  1. AbortPolicy (不干还发火)
    直接抛出拒绝异常(继承自RuntimeException),会中断调用者的处理过程,所以除非有明确需求,一般不推荐
  2. CallerRunsPolicy(不做,我自己上)
    调用者线程中运行当前被丢弃的任务,如果线程池已经关闭了,则直接丢弃该任务
  3. DiscardOledestPolicy(看看排期,移除最前面的,任务继续)
    丢弃队列中最老的,然后再次尝试提交新任务。
  4. DiscardPolicy(不做,就丢)
    丢弃无法加载的任务。
自定义拒绝策略
  1. netty自己实现的线程池里面私有的一个拒绝策略,单独启动一个新的临时线程来执行任务。
  private static final class NewThreadRunsPolicy implements RejectedExecutionHandler {
        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);
            }
        }
    }
  1. dubbo的一个例子,它直接继承的 AbortPolicy ,加强了日志输出,并且输出dump文件
public class AbortPolicyWithReport extends ThreadPoolExecutor.AbortPolicy {

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

队列选择
  1. 实现BlockingQueue接口的队列
  2. 参考Spring中的ThreadPoolTaskExecutor中队列的设计
    2.1 选择长度固定的LinkedBlockingQueue
    2.2 SynchronousQueue队列本身没有容量大小,放一个数据到队列中,不能够立马返回的,必须等待别人把放进去的数据消费掉了,才能够返回
    3.ArrayBlockingQueue 数组阻塞队列,该队列不能动态扩容,如果队列满了或者空时,take 和 put 都会被阻塞
    4.DelayedQueue
JDK已封装好的线程池存在的潜在风险
  1. FixedThreadPool 和 SingleThreadPool:
    允许的请求队列长度为 Integer.MAX_VALUE ,会堆积大量请求OOM
  2. CachedThreadPool 和 ScheduledThreadPool:
    允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量线程OOM
从系统安全角度出发,原则上都应该自己手动创建线程池
Tomat扩展Java线程池

Tomcat 线程池扩展了原生的 ThreadPoolExecutor,通过重写 execute 方法实现了自己的任务处理逻辑:

  1. 前 corePoolSize 个任务时,来一个任务就创建一个新线程。
  2. 再来任务的话,就把任务添加到任务队列里让所有的线程去抢,如果队列满了就创建临时线程。
  3. 如果总线程数达到 maximumPoolSize,则继续尝试把任务添加到任务队列中去。
  4. 如果缓冲队列也满了,插入失败,执行拒绝策略。
    Tomcat 线程池的 execute 方法的核心代码。
public class ThreadPoolExecutor extends java.util.concurrent.ThreadPoolExecutor {

  ...

  public void execute(Runnable command, long timeout, TimeUnit unit) {
      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)) {
                      submittedCount.decrementAndGet();
                      // 如果缓冲队列也满了,插入失败,执行拒绝策略。
                      throw new RejectedExecutionException("...");
                  }
              } 
          }
      }
}

Tomcat 的任务队列 TaskQueue 扩展了 Java 中的 LinkedBlockingQueue,并重写了 LinkedBlockingQueue 的 offer 方法,在合适的时机返回 false,返回 false 表示任务添加失败,这时线程池会创建新的线程

ublic class TaskQueue extends LinkedBlockingQueue<Runnable> {

  ...
   @Override
  // 线程池调用任务队列的方法时,当前线程数肯定已经大于核心线程数了
  public boolean offer(Runnable o) {

      // 如果线程数已经到了最大值,不能创建新线程了,只能把任务添加到任务队列。
      if (parent.getPoolSize() == parent.getMaximumPoolSize()) 
          return super.offer(o);

      // 执行到这里,表明当前线程数大于核心线程数,并且小于最大线程数。
      // 表明是可以创建新线程的,那到底要不要创建呢?分两种情况:

      //1. 如果已提交的任务数小于当前线程数,表示还有空闲线程,无需创建新线程
      if (parent.getSubmittedCount()<=(parent.getPoolSize())) 
          return super.offer(o);

      //2. 如果已提交的任务数大于当前线程数,线程不够用了,返回 false 去创建新线程
      if (parent.getPoolSize()<parent.getMaximumPoolSize()) 
          return false;

      // 默认情况下总是把任务添加到任务队列
      return super.offer(o);
  }

}
posted @ 2020-04-02 17:52  zendwang  阅读(101)  评论(0编辑  收藏  举报