线程池是什么?
简单来说,线程池是指提前创建若干个线程,当有任务需要处理时,线程池里的线程就会处理任务,处理完成后的线程并不会被销毁,而是继续等待下一个任务。由于创建和销毁线程都是消耗系统资源的,所以,当某个业务需要频繁进行线程的创建和销毁时,就可以考虑使用线程池来提高系统的性能啦;避免创建大量的线程增加开销,提高响应速度。。这样我们就不需要去new线程,直接通过线程池进行;
使用线程池有哪些好处?
首先在开发的过程中,为什么需要线程池呢?给我们带来了那些好处
- 提高系统的响应速度
- 如果每次多线程操作都创建一个线程,会浪费时间和消耗系统资源,而线程池可以减少这些操作
- 可以对多个线程进行统一管理,统一调度,提高线程池的可管理性
创建线程池的参数有哪些?
线程池是怎么创建的呢?一个是使用Executors,另外就是手动创建线程池,要了解其每个参数的含义。Executors创建线程池的话,要不就是对线程的数量没有控制,如CachedThreadPool,要不就是是无界队列,如FixedThreadPool。**对线程池数量和队列大小没有限制的话,容易导致OOM异常。**所以我们要自己手动创建线程池:
如何创建线程:
public class DemoThreadPoolExecutor {
private static final int CORE_POOL_SIZE = 5;
private static final int MAX_POOL_SIZE = 10;
private static final int QUEUE_CAPACITY = 100;
private static final Long KEEP_ALIVE_TIME = 1L;
public static void main(String[] args) {
// 使用线程池来创建线程
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
// 核心线程数为 :5
CORE_POOL_SIZE,
// 最大线程数 :10
MAX_POOL_SIZE,
// 等待时间 :1L
KEEP_ALIVE_TIME,
// 等待时间的单位 :秒
TimeUnit.SECONDS,
// 任务队列为 ArrayBlockingQueue,且容量为 100
new ArrayBlockingQueue<>(QUEUE_CAPACITY),
// 饱和策略为 CallerRunsPolicy
new ThreadPoolExecutor.CallerRunsPolicy()
);
线程池中各个数据的 解析:
1. corePoolSize :核心线程线程数
定义了最小可以同时运行的线程数量。(这些线程不会被删除)
2. maximumPoolSize :最大线程数
当队列中存放的任务达到队列容量时,当前可以同时运行的线程数量会扩大到最大线程数。
3. keepAliveTime :等待时间
当线程数大于核心线程数时,线程会去队列中拉取,拉取时间大于keepAliveTime 时间而没有拉取到任务时,则说明队列中没有任务,则可以将该线程直接摧毁
4. unit :时间单位。
keepAliveTime 参数的时间单位,包括 TimeUnit.SECONDS
、TimeUnit.MINUTES
、TimeUnit.HOURS
、TimeUnit.DAYS
等等。
5. workQueue :任务队列
任务队列,用来储存等待执行任务的队列。当队列数据满,并且最大线程也最大时,执行拒绝策略;所以如果垂涎这个情况,可适当调大任务队列值
6. threadFactory :线程工厂
线程工厂,用来创建线程,一般默认即可。
7. handler :拒绝策略
也称饱和策略;当提交的任务过多而不能及时处理时,可以通过定制策略来处理任务。
ThreadPoolExecutor 饱和策略 : 指当前同时运行的线程数量达到最大线程数量并且队列也已经被放满时,ThreadPoolTaskExecutor 所执行的策略。
常用的拒绝策略包括 :
- ThreadPoolExecutor.AbortPolicy: 抛出
RejectedExecutionException
来拒绝新任务的处理,是 Spring 中使用的默认拒绝策略。 - 直接让main方法去跑这个线程任务
- ThreadPoolExecutor.CallerRunsPolicy: 线程调用运行该任务的
execute
本身,也就是直接在调用execute
方法的线程中运行 (run
) 被拒绝的任务,如果执行程序已关闭,则会丢弃该任务。此策略提供简单的反馈控制机制,能够减缓新任务的提交速度,但可能造成延迟。若应用程序可以承受此延迟且不能丢弃任何一个任务请求,可以选择这个策略。 - ThreadPoolExecutor.DiscardPolicy: 不处理新任务,直接丢弃掉。
- ThreadPoolExecutor.DiscardOldestPolicy: 此策略将丢弃最早的未处理的任务请求。
线程池的任务逻辑
提交一个任务,线程池里存活的线程数小于核心线程数时,线程池会创建一个线程去处理提交的任务。
如果线程池中线程数已经等于核心线程数,一个新提交的任务,会被放进任务队列BlockingQueue排队等待执行。
当线程池里面存活的线程数已经等于核心线程数了,并且任务队列BlockingQueue也已经满了,判断线程数是否达到最大线程数,如果没达到,创建一个线程执行提交的任务。
如果当前的线程数达到了最大线程数,还有新的任务过来的话,直接采用拒绝策略处理。(下方会讲)
线程池创建线程时,会将线程封装成工作线程Worker,Worker在执行完任务后,会循环获取工作队列里的任务来执行:
如果当前线程数小于核心线程数,则使用take方法,无限制的阻塞获取BlockingQueue中的任务,取出任务或者被阻塞并挂起
如果当前线程数大于核心线程数,则使用poll(keepAliveTime,timeUnit)方法,带超时的阻塞获取BlockingQueue中的任务,取出任务或者超时,如果超时这个线程就会被销毁。会被超时销毁的情况:线程池允许核心线程超时销毁 或 当前线程数大于核心线程数
为什么推荐使用 ThreadPoolExecutor 来创建线程?(阿里规约)
规约一 :线程资源必须通过线程池提供,不允许在应用中自行显示创建线程。
使用线程池的好处是减少在创建和销毁线程上所消耗的时间以及系统资源开销,解决资源不足的问题。如果不使用线程池,有可能会造成系统创建大量同类线程而导致消耗完内存或者“过度切换”的问题。
规约二 :强制线程池不允许使用 Executors
去创建,而是通过 ThreadPoolExecutor
构造函数的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。
Executors 返回线程池对象的弊端如下:
FixedThreadPool
和SingleThreadExecutor
: 允许请求的队列长度为 Integer.MAX_VALUE,可能会堆积大量请求,从而导致 OOM。(超出内存大小)
CachedThreadPool
和ScheduledThreadPool
: 允许创建的线程数量为 Integer.MAX_VALUE,可能会创建大量线程,从而导致 OOM。
如何简单的拟定判断核心线程数
CPU 密集型任务(N+1):
这种任务消耗的主要是 CPU 资源,可以将线程数设置为 N(CPU 核心数)+1,比 CPU 核心数多出来的一个线程是为了防止线程偶发的缺页中断,或者其它原因导致的任务暂停而带来的影响。一旦任务暂停,CPU 就会处于空闲状态,而在这种情况下多出来的一个线程就可以充分利用 CPU 的空闲时间。
I/O 密集型任务(2N):
这种任务应用起来,系统会用大部分的时间来处理 I/O 交互,而线程在处理 I/O 的时间段内不会占用 CPU 来处理,这时就可以将 CPU 交出给其它线程使用。因此在 I/O 密集型任务的应用中,我们可以多配置一些线程,具体的计算方法是 2N
线程池的shut down与shut down now差别
shut down:对已经提交的任务不会产生影响,就是在队列中的任务都会执行完后才会停止关闭
shut down now:中断当前正在运行的线程,就是所有的任务都停止,直接关闭