使用线程池比手动创建线程好在哪里

第一点,反复创建线程系统开销比较大,每个线程创建和销毁都需要时间,如果任务比较简单,那么就有可能导致创建和销毁线程消耗的资源比线程执行任务本身消耗的资源还要大。
第二点,过多的线程会占用过多的内存等资源,还会带来过多的上下文切换,同时还会导致系统不稳定。

线程池解决问题思路
线程池用一些固定的线程一直保持工作状态并反复执行任务,
根据需要创建线程,控制线程总数量,避免占用过多内存资源

使用线程池的好处
第一点,线程池可以解决线程生命周期的系统开销问题,同时还可以加快响应速度
第二点,线程池可以统筹内存和 CPU 的使用,避免资源使用不当
第三点,线程池可以统一管理资源。

线程池的各个参数的含义

线程池 - 图1
线程创建的时机
**线程池 - 图2

4 种拒绝策略

拒绝时机
第一种情况是当我们调用 shutdown 等方法关闭线程池后
第二种情况是线程池没有能力继续处理新提交的任务,也就是工作已经非常饱和的时候
拒绝策略

  1. AbortPolicy 直接抛出一个类型为 RejectedExecutionException 的 RuntimeException
  2. DiscardPolicy 直接丢弃,没有任何提示,会丢数据
  3. DiscardOldestPolicy 丢弃任务队列中的头结点,(存活最久的)也会丢数据
  4. CallerRunsPolicy 谁提交任务,谁就负责执行任务

CallerRunsPolicy模式有两个优点
1.提交的任务不会被丢弃,这样也就不会造成业务损失。
2.谁提交,谁执行,执行任务比较耗时,期间提交任务的线程被占用,无法提交新的任务,降低了任务提交速度,给线程池一定缓存

6 种常见的线程池

  1. FixedThreadPool 固定线程数的线程池:核心线程数和最大线程数是一样的
  2. CachedThreadPool 可缓存线程池:几乎可以无限增加的的线程数,所以不需要阻塞队列
  3. ScheduledThreadPool 支持定时或周期性执行任务
  4. SingleThreadExecutor 使用唯一的线程去执行任务
  5. SingleThreadScheduledExecutor 一个线程的ScheduledThreadPool
  6. ForkJoinPool

ForkJoinPool
第一点非常适合执行可以产生子任务的任务.第一步是拆分也就是 Fork,第二步是汇总也就是 Join
第二点不同之处在于内部结构,之前的线程池所有的线程共用一个队列,但 ForkJoinPool 线程池中每个线程都有自己独立的任务队列

线程池常用的阻塞队列有哪些

线程池 - 图3
LinkedBlockingQueue 无界队列
SynchronousQueue 0容量队列
DelayedWorkQueue 延迟队列 内部采用的是“堆”的数据结构
ArrayBlockingQueue 有界队列 (自定义线程池时常用)


为什么不应该自动创建线程池

在大量请求进入的时候
FixedThreadPool 任务队列是LinkedBlockingQueue ,队列会堆积大量任务,占用大量内存,发送OOM
SingleThreadExecutor 任务队列是LinkedBlockingQueue ,队列会堆积大量任务,占用大量内存,发送OOM
CachedThreadPool 会创建无数线程,耗尽系统资源,导致无法创建新线程或者内存不足
ScheduledThreadPool 也是无界队列,问题同LinkedBlockingQueue
SingleThreadScheduledExecutor 也是无界队列,问题同LinkedBlockingQueue

这几种自动创建的线程池都存在风险,相比较而言,我们自己手动创建会更好,因为我们可以更加明确线程池的运行规则,不仅可以选择适合自己的线程数量,更可以在必要的时候拒绝新任务的提交,避免资源耗尽的风险。

合适的线程数量是多少

CPU 密集型任务:比如加密、解密、压缩、计算等;最佳的线程数为 CPU 核心数的 1~2 倍
耗时 IO 型任务:线程数 = CPU 核心数 *(1+平均等待时间/平均工作时间)

小结
线程的平均工作时间所占比例越高,就需要越少的线程;

线程的平均等待时间所占比例越高,就需要越多的线程;

针对不同的程序,进行对应的实际测试就可以得到最合适的选择。

线程池实现“线程复用”的原理

  1. 通过取 Worker 的 firstTask 或者通过 getTask 方法从 workQueue 中获取待执行的任务。
  2. 直接调用 task 的 run 方法来执行具体的任务(而不是新建线程)。