线程池Executor详解

113 阅读4分钟

前言

说道 java 多线程,就不得不提线程池 ThreadPoolExecutor。在 java 中,ThreadPoolExecutor 的顶级接口是 Executor,白话翻译执行器。Executor 里面只定义了一个方法void execute(Runnable command)——运行一个 Runable 对象。

一个简单的示例如下:

  ExecutorService executorService = Executors.newSingleThreadExecutor();
  for (int i = 0; i < 5; i++) {
      int finalI = i;
      executorService.execute(new Runnable() {
          @Override
          public void run() {
              System.out.println(finalI);
          }
      });
  }
  // 输出 0 1 2 3 4

在这个例子中,无论运行多少次,输出总是 012345,为什么呢?因为这里使用的单线程线程池 newSingleThreadExecutor,也就是只有一个线程运行,来任务时任务将会依次排队,一个一个执行。Executors 类为我们提供了多种封装好的线程池,常用的包括下面几个:

  • newFixedThreadPool:固定大小的线程池
  • newSingleThreadExecutor:单线程的线程池
  • newCachedThreadPool:缓存线程池
  • newScheduledThreadPool:定时定期执行的线程池

这些封装好的线程池都调用了 ThreadPoolExecutor 的初始化方法,只是参数不同罢了,下面,我们就来看看这个初始化方法的参数。

初始化方法

public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory,
                              RejectedExecutionHandler handler) {}

参数一共有 7 个,我们先来看前两个:核心线程数 corePoolSize 与 最大线程数 maximumPoolSize。所谓核心线程数,可以理解成常驻的工作线程数量。当有新任务时,如果当前线程数小于核心线程数,将会新建线程执行任务,反之则会加入阻塞队列排队——也就是第 5 个参数 workQueue。当队列满了,那么就会增加线程数至最大线程数 maximumPoolSize。如果已经达到了最大线程数,那么就会采取最后一个参数指定的拒绝策略。

默认的拒绝策略有 4 种,分别是:

  • CallerRunsPolicy:由调动线程直接处理
  • AbortPolicy:抛出异常
  • DiscardPolicy:丢弃
  • DiscardOldestPolicy:丢弃队列最前的任务

所以 Executors 封装好的线程池,主要的差别就在于核心线程数、最大线程数与阻塞队列的参数设置。

public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(nThreads, nThreads, // 核心线程数=最大线程数,所以线程数量固定
                                  0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue<Runnable>()); // 无界的阻塞队列
}

public static ExecutorService newSingleThreadExecutor() {
    return new FinalizableDelegatedExecutorService
        (new ThreadPoolExecutor(1, 1, // 核心线程数与最大线程数都为1,只会有一个线程
                                0L, TimeUnit.MILLISECONDS,
                                new LinkedBlockingQueue<Runnable>()));// 无界的阻塞队列
}

public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE, // 与阻塞队列配合,可能会创建非常多的线程处理任务
                                  60L, TimeUnit.SECONDS,
                                  new SynchronousQueue<Runnable>()); // 同步阻塞队列,入列时必须等另一个线程取走元素,出列时也必须等另一个线程放入元素
}

public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
    return new ScheduledThreadPoolExecutor(corePoolSize);
}

public ScheduledThreadPoolExecutor(int corePoolSize) {
    super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
          new DelayedWorkQueue()); // 延时队列
}

如何实现线程的复用

在 ThreadPoolExecutor 类中有个HashSet<Worker> workers,它存储了Worker对象,也就是真正的工作对象。这个对象继承了 Runnable 接口并重写了 run() 方法。在重写的 run()方法中,将会不断尝试从工作队列 workQueue 获取任务执行。

那既然通过工作队列排队执行任务,那么线程池是公平的吗?其实不是。因为在初始化新 worker 的时候,会将当前的 runable 对象作为第一个任务 firstTask,执行完这个 firstTask,才会接着执行工作队列中排队的任务:

final void runWorker(Worker w) {
    Thread wt = Thread.currentThread();
    Runnable task = w.firstTask; // 获取初始化时的任务
    w.firstTask = null;
    w.unlock(); // 允许中断
    boolean completedAbruptly = true; // 执行过程抛异常则为true,否则为false
    try {
        while (task != null || (task = getTask()) != null) { //firstTask未执行或者从工作队列中获取任务
            w.lock(); // 加锁
            if ((runStateAtLeast(ctl.get(), STOP) || // 如果线程池是STOP、TIDYING或者TERMINATED状态则中断
                  (Thread.interrupted() &&
                  runStateAtLeast(ctl.get(), STOP))) &&
                !wt.isInterrupted())
                wt.interrupt();
            try {
                beforeExecute(wt, task); // 钩子方法,可子类重写
                Throwable thrown = null;
                try {
                    task.run(); // 执行任务
                } catch (RuntimeException x) {
                    thrown = x; throw x;
                } catch (Error x) {
                    thrown = x; throw x;
                } catch (Throwable x) {
                    thrown = x; throw new Error(x);
                } finally {
                    afterExecute(task, thrown);// 钩子方法,可子类重写
                }
            } finally {
                task = null;
                w.completedTasks++; // 计数+1
                w.unlock(); // 解锁
            }
        }
        completedAbruptly = false;
    } finally {
        processWorkerExit(w, completedAbruptly);
    }
}

private Runnable getTask() {
    boolean timedOut = false; // Did the last poll() time out?

    for (;;) {
        int c = ctl.get();
        // ...
        try {
            Runnable r = timed ? // 是否设置了存活时间
                workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :// 是,阻塞到相应时间
                workQueue.take(); // 否,一直阻塞直到获取任务
            if (r != null)
                return r;
            timedOut = true;
        } catch (InterruptedException retry) {
            timedOut = false;
        }
    }
}

注意到 Worker 继承了 AQS 类并自己实现了一把不可重入的锁来维护中断状态。怎么维护的呢?在 interruptIdleWorkers()方法里,中断 worker 之前会调动 trylock()方法,只有尝试加锁成功才会中断线程。配合 runworker()中的加锁解锁,就不会中断正在运行的线程了。

线程池状态

注意到 runWorker()方法和 getTask()方法中都调用了ctl.get()。ctl 是线程池的状态控制字段,是个 AtomicInteger 对象。ctl 作为一个 32 位的整数类型,被分成了两部分:高 3 位表示线程池的状态,低 29 位记录工作线程的数量:

private static final int COUNT_BITS = Integer.SIZE - 3;
private static final int CAPACITY   = (1 << COUNT_BITS) - 1;

private static final int RUNNING    = -1 << COUNT_BITS; // 接受新任务并且对已有任务进行处理
private static final int SHUTDOWN   =  0 << COUNT_BITS; // 拒绝新任务,继续对已有任务处理
private static final int STOP       =  1 << COUNT_BITS; // 拒绝新任务,不继续对已有任务处理,中断正在进行的任务
private static final int TIDYING    =  2 << COUNT_BITS; // 所有任务终止,工作线程为0,执行terminated()方法
private static final int TERMINATED =  3 << COUNT_BITS; // terminated()方法执行结束

private static boolean isRunning(int c) {
    return c < SHUTDOWN;
}

private boolean compareAndIncrementWorkerCount(int expect) {
    return ctl.compareAndSet(expect, expect + 1);
}

由此,ThreadPoolExecutor 实现了 ExecutorService 接口中对于线程池状态监控和 shutdown() 等方法。