阿里为何不推荐使用Executors来创建线程池

目录

    • 线程池的启动流程
    • 任务的加入流程:
    • 定时任务线程池:
    • 线程池中线程数的设置多少合理
    • Executors 线程池创建工具
        • Executors.newCachedThreadPool()
        • Executors.newSingleThreadExecutor()
        • Executors.newFixedThreadPool(3)
        • Executors.newScheduledThreadPool(1)
    • 不推荐使用Executors来创建线程池
        • Executors.newCachedThreadPool()
        • Executors.newFixedThreadPool(1)
        • Executors.newSingleThreadExecutor()
        • Executors.newScheduledThreadPool(2)
    • 总结

线程的创建和销毁是十分耗性能的操作, 我们平常的工作在使用的线程也是生命周期还短暂的,如果不使用线程池就会频繁的创建销毁线程。 这很不划算,于是聪明的程序员们发明了线程池。

我们看下线程池的创建参数:

ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(                   
1,   10,  1,   TimeUnit.HOURS,   
new SynchronousQueue<Runnable>(),       
new ThreadPoolExecutor.AbortPolicy()    
);

看下线程池的几个参数的意义:

  1. 表示核心线程数,就是线程池中不会被回收的线程数
  2. 表示最大线程数, 就是线程池允许创建的最大线程数
  3. 第三个和第四个参数就是没有任务后的允许线程存活的时间
  4. 就是线程不够用后暂存任务的队列
  5. 最后如果队列不够用后的拒绝任务的策略

线程池的启动流程

  1. 创建线程池,设置各个参数, 此时没有一个线程
  2. execute或submit传入一个任务,同时会唤醒休眠的线程。
  3. 先判断线程数达到最大线程数没有, 如果没有达到就新建一个线程来运行任务
  4. 如果达到最大线程数,就开始会把任务加入队列中
  5. 任务队列也加满了,就会执行拒绝策略,
  6. 当某个线程运行完任务后, 会再次从队列中获取新的任务运行。
  7. 如果队列中没有任务,线程会休眠,休眠时间是传入的时间
  8. 某个线程休眠结束后,会再次从任务队列中获取任务,如果任务队列是空的, 则判断当前存活线程数是否大于核心线程数, 如果大于则这个线程就会死亡。
  9. 如果小于或者等于最小核心线程, 就会继续休眠。

任务的加入流程:

  1. 先判断线程数量小于核心线程, 就会调用addWorker方法
  2. 在addWorker 中会把任务包装成worker 。 worker中保存有线程和任务信息,内部的线程没有启动而已, 并把worker加入到HashSet中。
  3. 在addWorker中如果继续判断是否运行线程数到达最大线程,如果没有就启动最新创建的worker中的线程
  4. 如果在addWorker中启动线程成功就会任务加入就会完成。
  5. 如果在addWorker方法返回false, 有两种情况, 线程池状态不对,线程数量到达上限。 如果只是线程数量到达上限,就会把任务加入队列中。
  6. 如果状态不对,就会直接执行拒绝策略

定时任务线程池:

  1. 线程中任务的数量的上线没有限制
  2. 线程休眠等待时间10ms, 也就是定时任务的最小力度是10ms
  3. 实现了延时的DelayedWorkQueue来接收任务。 这个队列会根据对象的时间来排序任务的。
  4. 新建任务是先 ScheduledFuture对象, 再放入队列的。 之后在执行线程的

线程池中线程数的设置多少合理

程序中的任务我们分为IO密集型 和 运算密集型两种。

  • 如果是运算密集型的任务, 我们吧cpu的核心数量作为线程数就可以。 防止阻塞等因素还可以在加上1
  • IO密集型: 由于IO是不消耗cpu的,我们统计下任务中io耗时时间 和cpu耗时时间的比例, 如果比例是7:2 那么设置线程数为设置为 1+7/2 的值作为线程数比较合理,这样当IO运行完成后, cpu也运行完成了,不用互相等待浪费时间了。
  • 我们常见的web应用等等, 几乎都是io密集型的, 主要io浪费在数据库链接和http请求等。 所以这个时候设置线程数多一点也没有关系

但是程序中IO耗时和cpu的耗时比例不好统计, 我们只能够在现实中多加观察和以往的经验来设置个线程数, 并多多观察jvm的监控日志来设置合理的线程数

Executors 线程池创建工具

Executors.newCachedThreadPool()

创建的线程池核心线程0 , 最大线程是Integer.MaxValue。 线程空闲存活时间1分钟。 默认异常拒绝策略,使用SynchronousQueue队列。它的特点:

  • 每次添加任务如果没有空闲线程就会新建一个线程去执行。
  • SynchronousQueue是阻塞队列,加入任务的线程会阻塞住,直到其它线程从中取走任务才会结束阻塞
  • 他的线程创建上限近乎无限
    所以它适用于任务加入比较稳当且加入间隔短的场景
Executors.newSingleThreadExecutor()

核心和最大线程数都是1, 空闲存活时间为0 , 任务队列是无线长度的LinkedBlockingQueue。 默认异常拒绝策略。它的特点:

  • 只有一个线程
  • 近乎可以接收无限任务的队列, 可以堆积大量任务
    适用于任务持续加入但是任务数并不多的场景
Executors.newFixedThreadPool(3)

核心线程和最大线程数是你传入的参数。 其他参数和 Executors.newSingleThreadExecutor一样

Executors.newScheduledThreadPool(1)

这个是用于定时任务的线程池, 内部实现和上面三个都有不同。

  • 核心线程是传入的参数,最大线程是int上线, 默认存活时间是10毫秒, 任务队列使用自己实现的DelayedWorkQueue, 拒绝策略异常策略
  • 加入任务的时候,会把任务和定时时间构建一个RunnableScheduledFuture对象,再把这个对象放入DelayedWorkQueue队列中,
  • DelayedWorkQueue是一个有序队列, 他会根据内部的RunnableScheduledFuture的运行时间排序内部对象。
  • 任务加入后就会启动一个线程。 这个线程会从DelayedWorkQueue中获取一个任务。
  • DelayedWorkQueue内部是按照时间从前完后获取任务的。如果任务的中的时间还没有到。 获取的就是null。 获取任务结束,线程会休眠10毫秒。所以这个定时任务的执行最小间隔是10毫秒的。

由于Executors创建的线程池还多参数的设置都是什么Integer.MaxValue和无上限的BlockQuene等, 如果我们不加思考的使用,很容易把线程池用在错误的场景上, 因此阿里巴巴的编程手册上不推荐Executors创建线程池的方式, 它要求我们必须自己设置参数来创建线程池。

阿里的java手册中要求, 线程池必须自己设置参数来创建,严禁使用Executors来创建线程池。 这个要求是有道理的, 我们来看下为何不推荐

不推荐使用Executors来创建线程池

我们逐个看下executors创建的各个线程池的参数

Executors.newCachedThreadPool()

阿里为何不推荐使用Executors来创建线程池_第1张图片
最大线程数是Integer.MAX_VALUE, 并且任务队列是SynchronousQueue。 也就是说这个线程池对任务来着不拒,线程不够用就创建一个, 感觉就像一个豪横的富豪。 这就是问题所在了, 如果同一时刻应用的来了大量的任务, 这个线程池很容易就创建过多的线程, 而创建线程又是一个很耗性能的事情, 这就容易导致应用卡顿或者直接OOM

Executors.newFixedThreadPool(1)

阿里为何不推荐使用Executors来创建线程池_第2张图片
这个线程池到时没有上个线程池豪横了, 它定死了线程数量, 所以线程数量是不会超出的,但是它的任务队列是无界的LinkedBlockingQueue, 对于加进来的任务处理不过来就会存入任务队列中, 并且无限制的存入队列。 这个线程池感觉就是家里有地, 无论来多少货都往里面装。

这个线程池如果使用不当很容易导致OOM

Executors.newSingleThreadExecutor()

这个线程池只有一个线程, 比newFixedThreadPool还穷, 但是任务队列和上面一样, 没有限制, 很容易就使用不当导致OOM

Executors.newScheduledThreadPool(2)

阿里为何不推荐使用Executors来创建线程池_第3张图片
这个是定时任务的线程池, 没有定义线程创建数量的上线, 同时任务队列也没有定义上限, 如果前一次定时任务还没有完成, 后一个定时任务的运行时间到了, 它也会运行, 线程不够就创建。 这样如果定时任务运行的时间过长, 就会导致前后两个定时任务同时执行,如果他们之间有锁,还有可能出现死锁, 此时灾难就发生了。

举个例子:

  1. 某条公交路线平时只有两辆公交车在跑, 只有乘客太多的早晚高峰才会增加班车。
  2. 但是某一天 第一班车因为某种原因开的慢, 被第二班车追上了,并且两车发生事故堵在路上了,并且把路堵死了,
  3. 但是这个时候公交调度站不知道情况, 它只要看到站台有乘客等车就会增加一班车,
  4. 新增的班车来到事故点也被堵住了, 无法进行下去了
  5. 但是调度站只会根据乘客情况无限制发车,最终导致整个公交线路瘫痪。

所以使用这个线程池有一定的风险, 建议使用spring的定时任务模块, 他可以设置成第一个定时任务没有完成, 第二定时任务不触发。

总结

线程池是一个很好的重用资源的方式, 类似还有数据库连接池等, 但是业务场景是各种各样的,我们要根据不同的业务设置不同的线程池参数, 而不要使用Executors进行偷懒, Executors的很多的参数设置并不合理。

线程池使用还有一些注意问题和陷阱, 这些我会留在下篇博客中说明的。

你可能感兴趣的:(java,java,多线程)