线程池略略观
【为什么要使用线程池】
------传统线程创建方式的问题
- 反复创建线程系统开销比较大,每个线程创建和销毁都需要时间,如果任务比较简单,那么就有可能导致创建和销毁线程消耗的资源比线程执行任务本身消耗的资源还要大。
- 过多的线程会占用过多的内存等资源,还会带来过多的上下文切换,同时还会导致系统不稳定。
------线程池的优点
- 线程池可以解决线程生命周期的系统开销问题,同时因为线程复用,消除了创建线程的过程,可以加快响应速度。
- 线程池会根据配置和任务数量灵活地控制线程数量,可以统筹内存和 CPU 的使用,避免资源使用不当。
- 线程池可以统一管理资源。
【线程池参数】
corePoolSize |
核心线程数 |
maxPoolSize |
最大线程数 |
keepAliveTime+时间单位 |
空闲线程的存活时间 |
TheadFactory |
线程工厂,用于创建新线程 |
workQueue |
线程队列(一般未阻塞队列) |
Handler |
处理被拒绝的任务 |
------Spring配置:
<bean id="threadPool" class="org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor"> <!-- 核心线程数 --> <property name="corePoolSize" value="20" /> <!-- 最大线程数 --> <property name="maxPoolSize" value="80" /> <!-- 队列最大长度 >=mainExecutor.maxSize --> <property name="queueCapacity" value="5000" /> <!-- 线程池维护线程所允许的空闲时间 --> <property name="keepAliveSeconds" value="300" /> <!-- 线程池对拒绝任务(无线程可用)的处理策略 --> <property name="rejectedExecutionHandler"> <bean class="java.util.concurrent.ThreadPoolExecutor$CallerRunsPolicy" /> </property> </bean> |
------线程池基本工作流:
- 应用初始化后,当前线程池线程数为0;
- 有任务被提交后,创建核心线程执行任务;
- 当核心线程数达到上限后,任务添加到线程队列;
- 当线程队列内任务数达到上限后,创建非核心线程执行任务;
- 当线程池内总线程数达到最大线程数后,任务会被拒绝;
- 当任务渐渐被执行完,队列为空,而且当前线程数大于核心线程数之后,线程池会检测线程的keepAliveSeconds,如果线程空闲时间大于这个时间,线程将会被销毁。
- 线程渐渐地被销毁,线程池内存活线程等于核心线程数之后,则不继续做处理,线程空转,等待新的任务到来。
------ThreadFactory
线程工厂,主要作用是生产线程。
我们可以选择使用默认的线程工厂,创建的线程都会在同一个线程组,并拥有一样的优先级。
也可以选择自己定制线程工厂,以方便给线程自定义命名。
【拒绝策略】
AbortPolicy |
拒绝任务时,会直接抛出一个类型为 RejectedExecutionException 的 RuntimeException,可以感知到任务被拒绝了 |
DiscardPolicy |
当新任务被提交后直接被丢弃掉,有一定风险,可能会造成数据丢失 |
DiscardOldestPolicy |
丢弃任务队列中的头结点,通常是存活时间最长的任务,同样有风险 |
CallerRunsPolicy |
把这个任务交于提交任务的线程执行,也就是谁提交任务,谁就负责执行任务 |
【六种常见的线程池】
------FixedThreadPool
核心线程数和最大线程数一样,所以是固定线程数的线程池。
------CachedThreadPool
可缓存线程池,核心线程数为0,最大线程数为int最大值。
队列的容量为0,实际不存储任何任务,它只负责对任务进行中转和传递,所以效率比较高。
------ScheduledThreadPool
支持定时或周期性执行任务。
service.schedule(new Task(), 10, TimeUnit.SECONDS); |
表示延迟指定时间后执行一次任务 |
service.scheduleAtFixedRate(new Task(), 10, 10, TimeUnit.SECONDS); |
以固定的频率执行任务,它的第二个参数 initialDelay 表示第一次延时时间,第三个参数 period 表示周期,也就是第一次延时后每次延时多长时间执行一次任务。 |
service.scheduleWithFixedDelay(new Task(), 10, 10, TimeUnit.SECONDS); |
之前的 scheduleAtFixedRate 是以任务开始的时间为时间起点开始计时,时间到就开始执行第二次任务,而不管任务需要花多久执行; |
------SingleThreadExecutor
使用唯一的线程去执行任务,可保证执行顺序。
------SingleThreadScheduledExecutor
和第三个类似,只不过线程只有一个。
------ForkJoinPool
实现任务的分裂和汇总,充分利用多核CPU的计算能力。
【阻塞队列】
------LinkedBlockingQueue
无界队列,FixedThreadPool 和 SingleThreadExector使用。
------SynchronousQueue
SynchronousQueue是一个没有数据缓冲的BlockingQueue,生产者线程对其的插入操作put必须等待消费者的移除操作take,反过来也一样。
生产者和消费者互相等待对方,握手,然后一起离开。
对应线程池 CachedThreadPool。
------DelayedWorkQueue
对应线程池ScheduledThreadPool 和 SingleThreadScheduledExecutor。
DelayedWorkQueue 的特点是内部元素并不是按照放入的时间排序,而是会按照延迟的时间长短对任务进行排序,内部采用的是"堆"的数据结构。
【关闭线程池】
------shutdown()
安全地关闭一个线程池。
并不是立刻就被关闭,调用 shutdown() 方法后线程池会在执行完正在执行的任务和队列中等待的任务后才彻底关闭。
------isShutdown()
可以返回 true 或者 false 来判断线程池是否已经开始了关闭工作。
------isTerminated()
可以检测线程池是否真正"终结"了,这不仅代表线程池已关闭,同时代表线程池中的所有任务都已经都执行完毕了。
------awaitTermination()
判断线程池状态。
调用 awaitTermination 方法后当前线程会尝试等待一段指定的时间,如果在等待时间内,线程池已关闭并且内部的任务都执行完毕了,也就是说线程池真正"终结"了,那么方法就返回 true,否则超时返回 fasle。
------shutdownNow()
会给所有线程池中的线程发送 interrupt 中断信号,尝试中断这些任务的执行;
然后会将任务队列中正在等待的所有任务转移到一个 List 中并返回,我们可以根据返回的任务 List 来进行一些补救的操作,例如记录在案并在后期重试。
【线程复用原理】
在线程池中,同一个线程从BlockingQueue里面不断地提取任务;
核心内容在于对Tread进行了封装,每个线程都会去执行一个循环任务;
这个循环任务会不停地检查队列里面是否有待执行的任务,如果有,则执行其run方法,这样,线程就被串联了起来。