深入理解Java线程池

  1. 什么叫线程池
  2. 为什么使用
  3. 如何使用
  4. 深入参数分析
  5. 如何配置线程池大小
  6. 总结

什么叫线程池

一个线程集合, 统一管理线程的数量, 线程的生命周期, 并尽可能重用池中的线程. 我们常用的数据库连接池也是相同思想的产物.

为什么使用

  1. 一个线程在创建和销毁时, 非常消耗资源. 为了尽可以减少线程创建和销毁的次数, 多次重用同一线程, 将资源消耗分摊到多个任务上.
  2. 不使用线程池管理线程, 若运行的线程数量过大, 可能会导致系统资源消耗过大, 出现异常.

如何使用

  1. 使用如下方式创建线程池:
    public ThreadPoolExecutor(int corePoolSize,
                           int maximumPoolSize,
                           long keepAliveTime,
                           TimeUnit unit,
                           BlockingQueue<Runnable> workQueue,
                           ThreadFactory threadFactory,
                           RejectedExecutionHandler handler)
    //静态包装后的常用线程池方法
    Executors.newSingleThreadExecutor(); 
    Executors.newFixedThreadExecutor(int nthreads);
    Executors.newCachedThreadExecutor(); 
  2. 静态方法含义
  • Executors.newSingleThreadExecutor(): 创建一个线程池, 但只包含一个线程, 单线程执行所有任务, 若该线程异常结束, 重启一个新线程.
  • Executors.newFixedThreadExecutor(int nthreads): 创建一个线程池, 添加一个任务创建一个线程, 直到线程数量等于最大线程数, 线程数则保持不变. 若有线程异常结束, 则重启一个新线程.
  • Executors.newCachedThreadExecutor(): 创建一个线程池, 线程数量由jvm和执行的任务量决定, 任务增加时, 自动创建, 线程空闲超时(60秒)后, 自动回收.

深入参数分析

  1. 线程池的接口/类继承结构:
    ThreadPoolExecutor -> AbstractExecutorService -> ExecutorService -> Executor
    Executors 是ThreadPoolExecutor工厂方法实现.
  2. ThreadPoolExecutor 完整签名 参数讲解:
  • corePoolSize: 线程池中所保存的线程数量(包括空闲线程)
  • maximumPoolSize: 线程池中所允许的最大线程
  • keepAliveTime: 当线程数大于核心数时, 销毁线程的超时时间
  • unit: keepAliveTime 的时间单位
  • workQueue: 保存待执行的任务的任务队列
  • threadFactory: 执行创建线程时所使用的工厂类
  • handler: 拒绝执行的任务的处理类
  1. 线程池线程创建逻辑

    1. 当运行线程数 < corePoolSize 时: 直接添加新线程执行任务
    2. 当任务队列满, 运行线程数 >= corePoolSize且 < maximumPoolSize 时: 创建新线程执行任务.
    3. 当任务队列满, 运行线程数 >= corePoolSize且 >= maximumPoolSize 时: 执行拒绝策略
  2. 线程池线程超时逻辑

    1. 当线程池中线程保持 keepAliveTime 时间后, 自动销毁, 超时时间的单位由 unit 决定.
  3. 任务队列的类型

    1. ArrayBlockQueue: 有界队列, 可以防止资源耗尽, 但是难以调优, 不推荐使用.
    2. LinkedBlockQueue: 无界队列, 任务之间单独执行, 不相互影响.
    3. SynchronousQueue: 直接提交队列, 不保存任务, 直接提交线程. 但是每次插入必须等待另一个线程移除操作, 否则一直阻塞.
  4. 拒绝任务执行策略

    1. CallerRunsPolicy: 直接调用线程运行该任务. 该策略觉得该任务还可以再抢救. 一般会使用执行该execute的线程执行任务
    2. AbortPolicy: 丢弃任务, 抛出异常
    3. DiscardPolicy: 直接丢弃任务, 但是不抛出异常
    4. DiscardOldestPolicy: 将队列首部的旧任务剔除, 执行该任务. 请小心使用该策略.
  5. 静态方法的默认实现及输出演示:

    1. Executors.newSingleThreadExecutor() : 内部调用 new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue());

      ExecutorService executor = Executors.newSingleThreadExecutor();
      for (int i = 0; i < 4; i++) {
       final int index = i;
       executor.execute(() -> {
           System.out.print(Integer.toString(index) + Thread.currentThread().getId() + " ");
           try {
               Thread.sleep(1000);
           } catch (InterruptedException e) {
               e.printStackTrace();
           }
       });
      }
      executor.shutdown();
      // 输出:  013 113 213 313
      // 说明结果是同一线程, 顺序执行的
    2. Executors.newFixedThreadExecutor(int nthreads) : 内部调用 new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue());

      ExecutorService executor = Executors.newFixedThreadPool(2);
      for (int i = 0; i < 4; i++) {
       final int index = i;
       executor.execute(() -> {
           System.out.print(Integer.toString(index) + Thread.currentThread().getId() + " ");
           try {
               Thread.sleep(100);
           } catch (InterruptedException e) {
               e.printStackTrace();
           }
       });
      }
      executor.shutdown();
      // 输出:  011 112 211 312
      // 说明结果是两个线程, 独立顺序执行的
    3. Executors.newCachedThreadExecutor() : 内部调用 new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue());

      ExecutorService executor = Executors.newCachedThreadPool();
       for (int i = 0; i < 4; i++) {
           final int index = i;
           executor.execute(() -> {
               System.out.print(Integer.toString(index) + Thread.currentThread().getId() + " ");
               try {
                   Thread.sleep(100);
               } catch (InterruptedException e) {
                   e.printStackTrace();
               }
           });
       }
       executor.shutdown();
      // 输出:  011 112 213 314 
      // 说明结果是多个线程, 独立顺序执行的

如何配置线程池大小

  1. cpu密集型任务: 参考值 n(cpu数量) + 1
  2. io密集型任务 : 参考值 2 * n(cpu数量)
  3. 参数敏感时需要根据实际情况进行测试, 再具体调整参数

总结

  1. 推荐使用系统自带的 newSingleThreadExecutor(), newFixedThreadExecutor(), newCachedThreadExecutor() 三个创建线程池的静态方法创建线程, 能满足绝大部分情况.
  2. 自定义参数实现:
    1. 若对执行时间和顺序有要求, 建议使用优先级队列.
    2. 若任务较多可能超过系统限制, 建议使用有界队列.
    3. 实际情况建议多测试, 具体调整参数.

转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 zhao4xi@126.com

文章标题:深入理解Java线程池

文章字数:1.2k

本文作者:Zhaoxi

发布时间:2018-12-15, 15:00:51

最后更新:2019-09-21, 15:05:05

原始链接:http://zhao4xi.github.io/2018/12/15/深入理解Java线程池/

版权声明: "署名-非商用-相同方式共享 4.0" 转载请保留原文链接及作者。

目录