【并发编程四:Java中的线程池(1)】
【衔接上一章 【并发编程三:Java线程的状态及转换】】
学习路线
-
- 1.1Java中的线程池
-
- 1.1.1什么是线程池?
- 1.1.2为什么要有线程池?
- 1.2Java线程池之Executor框架
- 1.3Executor框架的接口与类结构
- 1.4线程池的7大参数
-
- (1)int corePoolSize,
- (2)BlockingQueue workQueue,
- (3)int maximumPoolSize,
- (4)long keepAliveTime,
- (5)TimeUnit unit,
- (6)ThreadFactory threadFactory,
- (7)RejectedExecutionHandler handler,
- 1.5线程池拒绝策略应用实践
1.1Java中的线程池
1.1.1什么是线程池?
白话一点理解就是一个池子,里面存放着已经创建好的线程,当有任务提交到线程池里面来的时候,池子中的某个线程就会主动去执行该任务,当提交过来的任务比较多,池子中的线程数不够用时,需要能自动扩充新的线程到池子中,当然也不能无限地扩充线程数量,能进行统一的配置,能配置最大线程个数,而当任务比较少时,池子中的线程个数就自动回收,把线程资源释放掉;并且通常情况下,为了能缓存提交过来的未被处理的任务,就需要有一个任务队列来存放,这就是一个线程池,你甚至可以自己来实现一个线程池;
1.1.2为什么要有线程池?
我们知道创建和销毁一个对象是很费时间的,特别是一些比较耗费资源的对象的创建和销毁,比如创建数据库连接,创建网络连接,创建线程等,所以就出现“池化技术”,即复用已经创建的对象,那么这样做能够带来3个好处:
第一:降低资源消耗,通过复用已经创建的线程降低线程创建和销毁造成的系统资源消耗;
第二:提高性能,当执行大量异步任务时线程池能够提供更好的性能,在不使用线程池时,每当需要执行异步任务时直接 new一个线程来运行,而线程的创建和销毁是需要开销的,而线程池里面的线程是可复用的,不需要每次执行异步任务时都重新创建和销毁线程,直接执行任务即可;
第三:方便线程管理,线程是不能随随便滥用的,当不停地创建线程可能导致系统资源消耗殆尽而崩溃,使用线程池可以限制创建的线程个数、动态新增线程数量等,提高了线程的可管理性;
1.2Java线程池之Executor框架
为了实现线程池和管理线程池,JDK给我们提供了基于Executor接口的一系列接口、抽象类、实现类,我们把它称作线程池的Executor框架,Executor框架本质上是一个线程池;
Java线程(java.lang.Thread)被一对一映射为本地操作系统内核线程,Java线程启动时会创建一个本地操作系统线程,操作系统会调度所有线程并将它们分配给可用的CPU执行,当该Java线程终止时,这个操作系统线程也会被回收;
实际上这是两层线程调度模型:
(1)上层Java线程的调度由Executor框架调度;
(2)下层操作系统的线程调度由操作系统调度;
Java的线程是这么设计的,包含两部分:
1、工作任务;(Runnable和Callable)
2、执行机制;(Thread、Executor框架)
1.3Executor框架的接口与类结构
- ava.util.concurrent (并发编程的工具) juc
- java.util.concurrent.atomic (变量的线程安全的原子性操作)
- java.util.concurrent.locks (用于锁定和条件等待同步等)
Executor : 执行人、执行者
1.4线程池的7大参数
//基于Executor框架实现线程池
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor( 5, 10, 15, TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(5), Executors.defaultThreadFactory(), new ThreadPoolExecutor.CallerRunsPolicy());
构造方法最多是7个参数;
(1)int corePoolSize,
指定线程池中的核心线程数量(最少的线程个数),线程池中会维护一个最小的线程数量,即使这些线程处理空闲状态,它们也不会被销毁,除非设置了allowCoreThreadTimeOut;
默认情况下,创建线程池之后,线程池中是没有线程的,需要提交任务之后才会创建线程;
在实际中如果需要线程池创建之后立即创建线程,可以通过以下两种方式:
prestartCoreThread():boolean prestartCoreThread(),初始化一个核心线程;
prestartAllCoreThreads():int prestartAllCoreThreads(),初始化所有核心线程;
(2)BlockingQueue workQueue,
任务队列,当核心线程全部繁忙时,由execute/submit方法提交的Runnable任务存放到该任务队列中,等待被核心线程来执行;
(3)int maximumPoolSize,
指定线程池中允许的最大线程数,当核心线程全部繁忙且任务队列存满之后,线程池会临时追加线程,直到总线程数达到maximumPoolSize这个上限;
(4)long keepAliveTime,
线程空闲超时时间,如果一个线程处于空闲状态,并且当前的线程数量大于corePoolSize,那么在指定时间后,这个空闲线程会被销毁;
(5)TimeUnit unit,
keepAliveTime的时间单位 (天、小时、分、秒…)
(6)ThreadFactory threadFactory,
线程工厂,用于创建线程,一般采用默认的即可,也可以自定义实现;
Executors.defaultThreadFactory(),
Executors.privilegedThreadFactory(),
(7)RejectedExecutionHandler handler,
拒绝策略(饱和策略),当任务太多来不及处理时,如何“拒绝”任务?
任务拒绝是线程池的保护措施,当核心线程corePoolSize正在执行任务、线程池的任务队列workQueue已满、并且线程池中的线程数达到maximumPoolSize时,就需要“拒绝”掉新提交过来的任务;
JDK提供了4种内置的拒绝策略:AbortPolicy、CallerRunsPolicy、DiscardOldestPolicy和DiscardPolicy;
-
1、AbortPolicy(默认):丢弃任务并抛出RejectedExecutionException异常,这是线程池默认的拒绝策略,在任务不能再提交的时候抛出异常,让开发人员及时知道程序运行状态,这样能在系统不能承载更大的并发量时,及时通过异常信息发现;
-
2、DiscardPolicy:直接丢弃任务,不抛出异常,使用此策略可能会使我们无法发现系统的异常状态,建议一些无关紧要的业务采用此策略;
-
3、DiscardOldestPolicy:丢弃任务队列中靠最前的任务,并执行当前任务,是否要采用此拒绝策略,根据实际业务是否允许丢弃老任务来评估和衡量;
-
4、CallerRunsPolicy: 交由任务的调用线程(提交任务的线程)来执行当前任务;这种拒绝策略会让所有任务都能得到执行,适合大量计算类型的任务执行,使用这种策略的最终目标是要让每个任务都能执行完毕,而使用多线程执行计算任务只是作为增大吞吐量的手段;
新来的任务可以用main线程去执行,不用线程池里面的线程执行;
除了上面的四种拒绝策略,还可以通过实现RejectedExecutionHandler接口,实现自定义的拒绝策略;
1.5线程池拒绝策略应用实践
一个发送短信验证码的需求,前端进行业务操作需要先获取验证码,由于是做营销活动,获取短信验证码请求量比较大,而发送短信验证码操作是提交到一个线程池中执行的,如果某一时刻获取验证码请求太多,可能导致线程池处理不过来,就会触发线程池拒绝策略,为了确保用户都能获取到验证码,当线程池处理不过来的时候,希望能给重试的机会,所以就自定义了一种线程池拒绝策略,做重试处理;
【衔接下一章【并发编程五:Java中的线程池(2)-线程的实现原理】】