Executor 类图

线程池原理

线程池中的核心线程数,当提交一个任务时,线程池创建一个新线程执行任务,直到当前线程数等于corePoolSize;如果当前线程数为 corePoolSize,继续提交的任务被保存到阻塞队列中,等待被执行;如果阻塞队列满了,那就创建新的线程执行当前任务;直到线程池中的线程数达到 maxPoolSize,这时再有任务来,只能执行 reject() 处理该任务。

线程池种类

  • newFixedThreadPool()

    初始化一个指定线程数的线程池,其中 corePoolSize == maxiPoolSize,使用 LinkedBlockingQuene 作为阻塞队列 特点:即使当线程池没有可执行任务时,也不会释放线程。

  • newCachedThreadPool()

    初始化一个可以缓存线程的线程池,默认缓存 60s,线程池的线程数可达到 Integer.MAX_VALUE,即 2147483647,内部使用 SynchronousQueue 作为阻塞队列; 特点:在没有任务执行时,当线程的空闲时间超过 keepAliveTime,会自动释放线程资源;当提交新任务时,如果没有空闲线程,则创建新线程执行任务,会导致一定的系统开销; 因此,使用时要注意控制并发的任务数,防止因创建大量的线程导致而降低性能。

  • newSingleThreadExecutor()

    初始化只有一个线程的线程池,内部使用 LinkedBlockingQueue 作为阻塞队列。 特点:如果该线程异常结束,会重新创建一个新的线程继续执行任务,唯一的线程可以保证所提交任务的顺序执行。

  • newScheduledThreadPool()

    特点:初始化的线程池可以在指定的时间内周期性的执行所提交的任务,在实际的业务场景中可以使用该线程池定期的同步数据。

  • ThreadPoolExecutor()

    默认线程池,可控制参数比较多,实际上是前面四种的模板,自定义时只需传入自己想要的参数即可。

初始化示例

1
2
3
4
5
6
7
// 使用Executors静态方法进行初始化
ExecutorService service = Executors.newSingleThreadExecutor();
// 常用方法
service.execute(new Thread());
service.submit(new Thread());
service.shutDown();
service.shutDownNow();

常用方法

  1. execute 与 submit 的区别
    • execute 只能提交 Runnable 类型的任务,而 submit 既能提交 Runnable 类型任务也能提交 Callable 类型任务。
    • execute 会直接抛出任务执行时的异常,submit 会吃掉异常,可通过 Future 的 get 方法将任务执行时的异常重新抛出。
    • submit 有返回值,而 execute 没有。
  2. shutDown 与 shutDownNow 的区别
    • shutDown:当线程池调用该方法时,线程池的状态则立刻变成 SHUTDOWN 状态。此时,则不能再往线程池中添加任何任务,否则将会抛出 RejectedExecutionException异常。但是,此时线程池不会立刻退出,直到添加到线程池中的任务都已经处理完成,才会退出。
    • shutDownNow:相当于调用每个线程的 interrupt() 方法。
  3. 其他方法
    • prestartAllCoreThreads():提前创建并启动所有核心线程。
    • setCorePoolSize() 和 setMaximumPoolSize():动态调整线程池容量大小。

关键参数

  • corePoolSize:核心线程数
  • maximumPoolSize:最大线程数
  • keepAliveTime:线程存活时间(在 corePore < * < maxPoolSize 情况下有用)
  • workQueue:阻塞队列(用来保存等待被执行的任务)

  • handler:当拒绝处理任务时的策略

四大阻塞队列(workQueue)

  • ArrayBlockingQueue:基于数组结构的有界阻塞队列,按 FIFO 排序任务
  • LinkedBlockingQuene:基于链表结构的阻塞队列,按 FIFO 排序任务
  • SynchronousQuene:一个不存储元素的阻塞队列,每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于 ArrayBlockingQuene
  • PriorityBlockingQuene:具有优先级的无界阻塞队列

四大拒绝策略(handler)

  • ThreadPoolExecutor.AbortPolicy:默认,队列满了丢弃任务并抛出 RejectedExecutionException 异常
  • ThreadPoolExecutor.DiscardPolicy:队列满了丢弃任务,但是不抛出异常
  • ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试加入队列(重复此过程)
  • ThreadPoolExecutor.CallerRunsPolicy:如果添加到线程池失败,那么主线程会自己去执行该任务

合理设置线程池大小

线程等待时间所占比例越高,需要越多线程。线程 CPU 时间所占比例越高,需要越少线程。

一般需要根据任务的类型来配置线程池大小: 如果是 CPU 密集型任务,就需要尽量压榨 CPU,参考值可以设为 NCPU+1;如果是 IO 密集型任务,参考值可以设置为 2*NCPU。

补充:CPU 密集型任务的特点是要进行大量的计算,消耗CPU资源,比如计算圆周率、对视频进行高清解码等等,全靠 CPU 的运算能力;涉及到网络、磁盘 IO 的任务都是 IO 密集型任务,这类任务的特点是 CPU 消耗很少,任务的大部分时间都在等待 IO 操作完成(因为 IO 的速度远远低于 CPU 和内存的速度)。