前言
说道 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() 等方法。