Executor--JCIP C06读书笔记
[本文是我对Java Concurrency In Practice 6.1/6.2的归纳和总结. 转载请注明作者和出处, 如有谬误, 欢迎在评论中指正. ]
为什么需要使用线程池? one-thread-per-request可能带来的问题:
1. 线程的创建和销毁会占用一定的资源. 如果请求频繁而对请求的处理是轻量级的(大多的web请求符合该情形), 创建一个线程处理请求后将其销毁的方式是不划算的.
2. 过多的线程导致线程切换频繁, 用于处理请求的CPU时间反而会减少. 如果当前的线程数已经让CPU处于忙碌状态, 那么增加更多的线程不会改善应用的性能.
3. 过多的线程会导致系统稳定性下降.
综上, 我们需要考虑将创建好的线程组织成线程池, 当请求来临时从池中取出线程处理请求, 处理完毕后将线程归还给线程池, 而不是销毁. 另外我们可以限制线程池中的线程数, 以克服线程过多时性能和稳定性下降的缺陷.
Executor
java的Executor Framework包含多个线程池的实现, 所有线程池都派生自Executor接口. Executor接口只定义了一个方法: execute(Runnable task). Executor接口解耦了任务提交和任务执行. Executor接口是基于生产者消费者模式实现的, 提交任务的线程为生产者, 执行任务的线程为消费者. 使用示例:
class TaskExecutionWebServer { private static final int NTHREADS = 100; // 创建线程池 private static final Executor exec = Executors.newFixedThreadPool(NTHREADS); public static void main(String[] args) throws IOException { ServerSocket socket = new ServerSocket(80); while (true) { final Socket connection = socket.accept(); Runnable task = new Runnable() { public void run() { handleRequest(connection); } }; // 将任务提交给线程池执行 exec.execute(task); } } }
ExecutorService和线程池的具体实现
ExecutorService接口继承自Executor, 而预定义的线程池类大多实现了ExecutorService接口.
调用Executors类的静态方法可以获取预定义的线程池:
1. newFixedThreadPool. 调用该方法创建最大线程数固定的线程池.
2. newCachedThreadPool. 调用该方法创建可伸缩式线程池, 当线程池中线程的数量超过程序所需时, 会自动销毁多余的线程; 当线程池中的线程不能需要时再创建新的线程执行提交的任务. 该线程池没有最大线程数的限定.
3. newSingleThreadExecutor. 调用该方法创建仅包含一个线程的线程池, 提交给该线程池执行的任务, 都将在这一单个线程中完成处理.
4. newScheduledThreadPool. 调用该方法创建最大线程数固定且支持延迟和周期性重复执行任务的线程池.
ExecutorService接口提供了一些生命周期管理的方法:
1. 关闭线程池. shutdown()方法在关闭前允许执行以前提交的任务, 包括那些已提交但尚未开始执行的任务. 而shutdownNow()方法阻止尚未开始执行的任务启动并试图停止当前正在执行的任务, 返回从未开始执行的任务的列表. 线程池关闭后将拒绝接受新任务, isShutdown()可用于判断线程池是否已关闭.
2. 当线程池已关闭, 并且所有提交给线程池的任务都已完成时, 线程池转变为终止状态. 调用ExecutorService的awaitTermination方法, 将使得当前线程阻塞, 直到线程池转变为终止状态. 通常在调用shutdown方法后紧接着调用awaitTermination方法. isTerminated()用于检测线程池是否处于终止状态.
使用ExecutorService的例子:
class LifecycleWebServer { private final ExecutorService exec = ...; public void start() throws IOException { ServerSocket socket = new ServerSocket(80); while (!exec.isShutdown()) { try { final Socket conn = socket.accept(); exec.execute(new Runnable() { public void run() { handleRequest(conn); } }); } catch (RejectedExecutionException e) { // 线程池关闭后提交任务将抛出RejectedExecutionException异常 if (!exec.isShutdown()) log("task submission rejected", e); } } } public void stop() { exec.shutdown(); } void handleRequest(Socket connection) { Request req = readRequest(connection); // 如果是关闭请求, 就关闭线程池, 否则分发该请求 if (isShutdownRequest(req)) stop(); else dispatchRequest(req); } }
Timer和ScheduledThreadPoolExecutor的比较
两者都可以用于延时或周期性重复执行某个任务, 但是Timer存在一些缺陷:
1. Timer基于绝对时间来安排任务的调度, 因此系统时钟的改变会对其产生影响. ScheduledThreadPoolExecutor基于相对时间进行任务的调度.
2. Timer创建单一的线程执行定时任务. 假如Timer对象以10ms的间隔重复执行某个任务, 但是其中的一次执行花去了40ms, 这就意味着少执行了至少4次重复任务. ScheduledThreadPoolExecutor可以使用多个线程执行定时任务.
3. 如果在执行任务的过程中抛出运行时异常, Timer的线程会被终止且没有恢复机制. ScheduledThreadPoolExecutor对这种情形做了适当的处理.
综上, JDK5.0之后, 几乎没有理由继续使用Timer调度定时任务了.