1、为什么使用线程池(线程池好处)?
线程池做的工作主要是控制运行的线程的数量,处理过程中将任务加入队列,然后在线程创建后启动这些任务,如果提交的任务超过了线程能执行的最大数量,超出的数量的任务会被提交到一个阻塞队列排队等候,等其他线程执行完毕,再从队列中取出任务来执行.
主要特点为:线程复用、控制最大并发数、管理线程.
-
降低资源消耗。 通过
重复利用已经创建的线程,降低线程创建的和销毁
造成的消耗。例如,工作线程Woker会无线循环获取阻塞队列中的任务来执行。 - 提高响应速度。 当任务到达时,
任务可以不需要等到线程创建就能立即执行
。 - 提高线程的可管理性。 线程是稀缺资源,Java的线程池可以对线程资源进行
统一分配
、调优
和监控
。
2、线程池的工作流程?
一个新的任务到线程池时,线程池的处理流程如下:
判断核心线程池里的线程是否
都在执行任务(即核心线程是否已满), 如果不是,创建一个新的工作线程来执行任务
。如果核心线程池里的线程都在执行任务(即核心线程已满),则进入下个流程。判断阻塞队列是否已满。
如果阻塞队列没有满,则将新提交的任务存储在阻塞队列中
。如果阻塞队列已满,则进入下个流程。判断线程池里的线程是否都处于工作状态(即线程池是否已满)。
如果没有,则创建一个新的工作线程来执行任务。如果已满,则交给饱和策略来处理这个任务。
线程池工作流程图如下:
3、线程池实现类
线程池的核心实现类是ThreadPoolExecutor类
,用来执行提交的任务。因此,任务提交到线程池时,具体的处理流程是由ThreadPoolExecutor类
的execute()方法去完成的。
- 如果当前运行的线程少于
corePoolSize
,则创建新的工作线程来执行任务(执行这一步骤需要获取全局锁)。 - 如果当前运行的线程大于或等于
corePoolSize
,而且BlockingQueue
未满,则将任务加入到BlockingQueue
中。 - 如果
BlockingQueue
已满,而且当前运行的线程小于maximumPoolSize
,则创建新的工作线程来执行任务(执行这一步骤需要获取全局锁)。 - 如果当前运行的线程大于或等于
maximumPoolSize
,任务将被拒绝
,并调用RejectExecutionHandler.rejectExecution()方法
。即调用饱和策略对任务进行处理。
工作线程(Worker): 线程池在创建线程时,会将线程封装成工作线程Woker。Woker在执行完任务后,不是立即销毁而是循环获取阻塞队列里的任务来执行
。阻塞队列为空,就阻塞等待,直到阻塞队列中有任务。
4、线程池的创建(7个参数)
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
4.1、corePoolSize :核心线程池的基本大小
- 线程池中会维护一个最小的线程数量,即使这些线程处理空闲状态,他们也不会 被销毁,除非设置了allowCoreThreadTimeOut。这里的最小线程数量即是corePoolSize。
- 提交一个任务到线程池时,线程数小于
corePoolSize,
会创建一个新的线程来执行任务。注意: 即使有空闲的核心线程
能执行该任务,也会创建新的线程。 - 如果线程池中的线程数已经大于或等于
corePoolSize
,则不会创建新的线程。 - 如果调用了线程池的
prestartAllCoreThreads()方法
,线程池会提前创建并启动所有基本线程
。
4.2、maximumPoolSize(线程池的最大数量): 线程池允许创建的最大线程数。
- 如果使用无界的阻塞队列,
该参数没有什么效果
。 - 一个任务被提交到线程池以后,首先会找有没有空闲存活线程,如果有则直接执行,如果没有则会缓存到工作队列(后面会介绍)中,如果工作队列满了,才会创建一个新线程,然后从工作队列的头部取出一个任务交由新线程来处理,而将刚提交的任务放入工作队列尾部。线程池不会无限制的去创建新线程,它会有一个最大线程数量的限制,这个数量即由maximunPoolSize指定。
4.3、keepAliveTime(线程活动保持时间):
- 多余的空闲线程的存活时间。如果
任务多而且任务的执行时间比较短
,可以调大keepAliveTime
,提高线程的利用率。 - 当空闲时间达到keepAliveTime时,多余的线程将被销毁,直至只剩下corePoolSize的数量为止。
4.4、unit(线程活动保持时间的单位):
- 可选单位有
TimeUnit.DAYS; //天
TimeUnit.HOURS; //小时
TimeUnit.MINUTES; //分钟
TimeUnit.SECONDS; //秒
TimeUnit.MILLISECONDS; //毫秒
TimeUnit.MICROSECONDS; //微妙
TimeUnit.NANOSECONDS; //纳秒
4.5、workQueue(工作队列):
新任务被提交后,会先进入到此工作队列中,任务调度时再从队列中取出任务。jdk中提供了四种工作队列:
ArrayBlockingQueue:
基于数组结构的有界阻塞队列,按FIFO(先进先出)原则对任务进行排序。使用该队列,线程池中能创建的最大线程数为maximumPoolSize
。LinkedBlockingQueue:
基于链表结构的无界阻塞队列,按FIFO(先进先出)原则对任务进行排序,吞吐量高于ArrayBlockingQueue
。使用该队列,线程池中能创建的最大线程数为corePoolSize
。静态工厂方法Executors.newFixedThreadPool()
使用了这个队列。SynchronousQueue:
一个不存储元素的阻塞队列。添加任务的操作必须等到另一个线程的移除操作
,否则添加操作一直处于阻塞状态
。静态工厂方法Executors.newCachedThreadPool()
使用了这个队列。吞吐量通常要高于LinkedBlockingQuene;PriorityBlokingQueue:
一个支持优先级的无界阻塞队列。使用该队列,线程池中能创建的最大线程数为corePoolSize
。
4.6、threadFactory 线程工厂
创建一个新线程时使用的工厂,可以用来设定线程名、是否为daemon线程等等。可自定义。
4.7、handler(饱和策略,或者又称拒绝策略):
当工作队列中的任务已到达最大限制,并且线程池中的线程数量也达到最大限制,这时如果有新任务提交进来,该如何处理呢。这里的拒绝策略,就是解决这个问题的。
通过实现RejectedExecutionHandler接口可以自定义拒绝策略,但是拒绝策略是一种处理较为极端的情况的方式,正常情况下尽量通过合理的设计线程池来防止溢出。
jdk中提供了4中拒绝策略:
- CallerRunsPolicy
该策略下,在调用者线程中直接执行被拒绝任务的run方法,除非线程池已经shutdown,则直接抛弃任务。
- AbortPolicy
该策略下,直接丢弃任务,并抛出RejectedExecutionException异常。
- DiscardPolicy
该策略下,直接丢弃任务,什么都不做。
- DiscardOldestPolicy
该策略下,抛弃进入队列最早的那个任务,然后尝试把这次拒绝的任务放入队列
5、线程池的种类
JAVA8中JDK有5种定义好的线程池,分别是:
- FixedThreadPool: 创建一个定长线程池,可控制线程的最大并发数,超出的线程会在队列中等待,newFixedThreadPool创建的线程池corePoolSize和MaxmumPoolSize是相等的,它使用的任务队列是LinkedBlockingQueue。
- SingleThreadPool:创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务都按照指定顺序执行。newSingleThreadExecutor将corePoolSize和MaxmumPoolSize都设置为1,它使用的任务队列是LinkedBlockingQueue。
- CacheThreadPool:创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则创建新线程。newCachedThreadPool将corePoolSize设置为0,MaxmumPoolSize设置为Integer.MAX_VALUE,它使用的是SynchronousQUeue,也就是说来了任务就创建线程运行,如果线程空闲超过60秒,就销毁线程。
- ScheduledThreadPool:创建一个可以按时间周期执行任务的线程池,使用的任务队列是DelayedWorkQueue。
- WorkStealingPool:java8新增,使用目前机器上可以的处理器作为他的并行级别。
以上五种线程池都可以使用Executors来创建: