创建和销毁线程非常损耗性能, 那有没有可能复用一些已经被创建好的线程呢? 答案是肯定的, 那就是线程池.
另外, 线程的创建需要开辟虚拟机栈, 本地方法栈, 程序计数器等线程私有的内存空间, 在线程销毁时需要回收这些系统资源, 频繁地创建销毁线程会浪费大量资源, 而通过复用已有线程可以更好地管理和协调线程的工作.
线程池主要解决两个问题:
1, 当执行大量异步任务时线程池能够提供很好的性能.
2, 线程池提供了一种资源限制和管理的手段, 比如可以限制线程的个数, 动态新增线程.
创建线程池
为了更方便的使用线程池, JDK 中给我们提供了一个线程池的工厂类 Executors. 在 Executors 中定义了多个静态方法, 用来创建不同配置的线程池. 常见有以下几种:
newSingleThreadExecutor
创建一个单线程化的线程池, 它只会用唯一的工作线程来执行任务, 保证所有任务按先进先出的顺序执行.
执行上述代码结果如下, 可以看出所有的 task 始终是在同一个线程中被执行的.
newCachedThreadPool
创建一个可缓存线程池, 如果线程池长度超过处理需要, 可灵活回收空闲线程, 若无可回收, 则新建线程.
执行效果如下:
从上面日志中可以看出, 缓存线程池会创建新的线程来执行任务. 但是如果将代码修改一下, 在提交任务之前休眠 1 秒钟, 如下:
再次执行则打印日志同 SingleThreadPool 一模一样, 原因是提交的任务只需要 500 毫秒即可执行完毕, 休眠 1 秒导致在新的任务提交之前, 线程 "pool-1-thread-1" 已经处于空闲状态, 可以被复用执行任务.
newFixedThreadPool
创建一个固定数目的, 可重用的线程池.
上述代码创建了一个固定数量 3 的线程池, 因此虽然向线程池提交了 10 个任务, 但是这 10 个任务只会被 3 个线程分配执行, 执行效果如下:
newScheduledThreadPool
创建一个定时线程池, 支持定时及周期性任务执行.
上面代码创建了一个线程数量为 2 的定时任务线程池, 通过 scheduleAtFixedRate 方法, 指定每隔 500 毫秒执行一次任务, 并且在 5 秒钟之后通过 shutdown 方法关闭定时任务. 执行效果如下:
上面这几种就是常用到的线程池使用方式, 但是! 在阿里 Java 开发手册中已经严禁使用 Executors 来创建线程池, 这是为什么? 要回答这个问题需要先了解线程池的工作原理.
线程池工作原理分析
线程池的构造器如下:
corePoolSize: 表示核心线程数量
maximumPoolSize: 表示线程池最大能够容纳同时执行的线程数, 必须大于或等于 1. 如果和 corePoolSize 相等即是固定大小线程池
keepAliveTime: 表示线程池中的线程空闲时间, 当空闲时间达到此值时, 线程会被销毁直到剩下 corePoolSize 个线程
unit: 用来指定 keepAliveTime 的时间单位, 有 MILLISECONDS,SECONDS,MINUTES,HOURS 等
workQueue: 等待队列, BlockingQueue 类型. 当请求任务数大于 corePoolSize 时, 任务将被缓存在此 BlockingQueue 中
threadFactory: 线程工厂, 线程池中使用它来创建线程, 如果传入的是 null, 则使用默认工厂类 DefaultThreadFactory
handler: 执行拒绝策略的对象. 当 workQueue 满了之后并且活动线程数大于 maximumPoolSize 的时候, 线程池通过该策略处理请求
需要注意的是当 ThreadPoolExecutor 的 allowCoreThreadTimeOut 设置为 true 时, 核心线程超时后也会被销毁.
流程解析
当我们调用 execute 或者 submit, 将一个任务提交给线程池, 线程池收到这个任务请求后, 有以下几种处理情况:
1, 当前线程池中运行的线程数量还没有达到 corePoolSize 大小时, 线程池会创建一个新线程执行提交的任务, 无论之前创建的线程是否处于空闲状态.
上面代码创建了 3 个固定数量的线程池, 每次提交的任务耗时 100 毫秒. 每次提交任务之前都会延迟 2 秒, 保证线程池中的工作线程都已经执行完毕, 但是执行效果如下:
可以看出虽然线程 1 和线程 2 都已执行完毕并且处于空闲状态, 但是线程池还是会尝试创建新的线程去执行新提交的任务, 直到线程数量达到 corePoolSize.
2, 当前线程池中运行的线程数量已经达到 corePoolSize 大小时, 线程池会把任务加入到等待队列中, 直到某一个线程空闲了, 线程池会根据我们设置的等待队列规则, 从队列中取出一个新的任务执行.
上述代码提交的任务耗时 4 秒, 因此前 2 个任务会占用线程池中的 2 个核心线程. 此时有新的任务提交给线程池时, 任务会被缓存到等待队列中, 结果如下:
可以看到红框 1 中通过 2 个核心线程直接执行提交的任务, 因此等待队列中的数量为 0; 而红框 2 中表明, 此时核心线程都已经被占用, 新提交的任务都被放入等待队列中.
3, 如果线程数大于 corePoolSize 数量但是还没有达到最大线程数 maximumPoolSize, 并且等待队列已满, 则线程池会创建新的线程来执行任务.
上述代码创建了一个核心线程数为 2, 最大线程数为 10, 等待队列长度为 2 的线程池. 执行效果如下:
解释说明:
1 处表示线程数量已经达到 corePoolSize
2 处表明等待队列已满
3 处会创建新的线程执行任务
4, 最后如果提交的任务, 无法被核心线程直接执行, 又无法加入等待队列, 又无法创建 "非核心线程" 直接执行, 线程池将根据拒绝处理器定义的策略处理这个任务. 比如在 ThreadPoolExecutor 中, 如果你没有为线程池设置 RejectedExecutionHandler. 这时线程池会抛出 RejectedExecutionException 异常, 即线程池拒绝接受这个任务.
修改最大线程数为 3, 并提交 6 次任务给线程池, 执行效果如下:
程序会报异常 RejectedExecutionException, 拒绝策略是线程池的一种保护机制, 目的就是当这种无节制的线程资源申请发生时, 拒绝新的任务保护线程池.
为何禁止使用 Executors
现在再回头看一下为何在阿里 Java 开发手册中严禁使用 Executors 工具类来创建线程池. 尤其是 newFixedThreadPool 和 newCachedThreadPool 这两个方法.
比如如下使用 newFixedThreadPool 方法创建线程的案例:
上述代码创建了一个固定数量为 2 的线程池, 并通过 for 循环向线程池中提交 100 万个任务. 通过 java -Xms4m -Xmx4m FixedThreadPoolOOM 执行上述代码:
可以发现当任务添加到 7 万多个时, 程序发生 OOM. 看一下 newSingleThreadExecutor 和 newFixedThreadPool() 的具体实现, 如下:
可以看到传入的是一个无界的阻塞队列, 理论上可以无限添加任务到线程池. 当核心线程执行时间很长 (比如 sleep10s), 则新提交的任务还在不断地插入到阻塞队列中, 最终造成
OOM.
再看下 newCachedThreadPool 会有什么问题.
同样会报 OOM, 只是错误的 log 信息有点区别: 无法创建新的线程.
看一下 newCachedThreadPool 的实现:
可以看到, 缓存线程池的最大线程数为 Integer 最大值. 当核心线程耗时很久, 线程池会尝试创建新的线程来执行提交的任务, 当内存不足时就会报无法创建线程的错误.
总结
线程池是一把双刃剑, 使用得当会使代码如虎添翼; 但是使用不当将会造成重大性灾难. 而剑柄是握在开发者手中, 只有理解线程池的运行原理, 熟知它的工作机制与使用场景, 才会使这把双刃剑发挥更好的作用.
来源: https://www.cnblogs.com/rh1910362960/p/13430529.html