java基础08-线程池
文章目录
一、什么是线程池
线程池可以看做是线程的集合。它的工作主要是控制运行的线程的数量,处理过程中将任务放入队列,然后在线程创建后 启动这些任务,如果线程数量超过了最大数量超出数量的线程排队等候,等其它线程执行完毕, 再从队列中取出任务来执行。他的主要特点为:线程复用,控制最大并发数,管理线程。
二、创建线程池
线程池可以自动创建也可以手动创建,自动创建体现在Executors工具类中,常见的可以创建newFixedThreadPool、newCachedThreadPool、newSingleThreadExecutor、newScheduledThreadPool:
|
|
ThreadPoolExecutor
手动创建体现在可以灵活设置线程池的各个参数,体现在代码中即ThreadPoolExecutor类构造器上各个实参的不同:
|
|
ThreadPoolExecutor中的参数解释
-
corePoolSize:核心线程数,也是线程池中常驻的线程数(即使没有任务,核心线程也会保持存活),线程池初始化时默认是没有线程的,当任务来临时才开始创建线程去执行任务。
-
maximumPoolSize:最大线程数,在核心线程数的基础上可能会额外增加一些非核心线程,需要注意的是只有当workQueue队列填满时才会创建多于corePoolSize的线程(线程池总线程数不超过maxPoolSize)
-
keepAliveTime:非核心线程的空闲时间超过keepAliveTime就会被自动终止回收掉,注意当corePoolSize=maxPoolSize时,keepAliveTime参数也就不起作用了(因为不存在非核心线程);
-
unit:keepAliveTime的时间单位
-
workQueue:用于保存任务的队列,可以为无界、有界、同步移交三种队列类型之一,当池子里的工作线程数大于corePoolSize时,这时新进来的任务会被放到队列中
-
SynchronousQueue(同步移交队列):队列不作为任务的缓冲方式,可以简单理解为队列长度为零
-
LinkedBlockingQueue(无界队列):队列长度不受限制,当请求越来越多时(任务处理速度跟不上任务提交速度造成请求堆积)可能导致内存占用过多或OOM
-
ArrayBlockintQueue(有界队列):队列长度受限,当队列满了就需要创建多余的线程来执行任务
-
-
threadFactory:创建线程的工厂类,默认使用Executors.defaultThreadFactory(),也可以使用guava库的ThreadFactoryBuilder来创建
-
handler:线程池无法继续接收任务(队列已满且线程数达到maximunPoolSize)时的饱和策略,取值有AbortPolicy、CallerRunsPolicy、DiscardOldestPolicy、DiscardPolicy
-
AbortPolicy:中断抛出异常
-
DiscardPolicy:默默丢弃新提交的任务,不进行任何通知
-
DiscardOldestPolicy:丢弃掉在队列中存在时间最久的任务
-
CallerRunsPolicy:让提交任务的线程去执行任务(对比前三种比较友好一丢丢)
-
为什么在工作队列满了后才创建大于corePoolSize小于maximumPoolSize的新线程?
资源节约
创建和管理线程是有代价的,包括内存开销和上下文切换的开销。通过限制线程数在
corePoolSize以内,可以减少这些开销,避免系统资源被过多的线程占用,从而提高整体系统的稳定性和性能。任务调度效率
如果当前线程数已经达到
corePoolSize,说明系统的负载是相对稳定的,此时将任务放入队列中等待处理,可以避免频繁地创建和销毁线程。这样可以提高任务调度的效率,因为线程创建和销毁是较为昂贵的操作。负载平衡
通过使用工作队列,可以平衡负载。当系统负载增加时,队列可以暂时缓冲任务,避免立即创建大量线程来处理突发的任务。这有助于防止系统在短时间内创建过多的线程,导致资源被耗尽。
更好的控制
通过这种方式,线程池可以更好地控制线程的增长。只有在工作队列已满且仍有新的任务提交时,才会创建额外的线程。这种策略使得线程池能够在需要时扩展线程数,但又不会轻易超过
maximumPoolSize,从而保持系统的稳定性。提高响应能力
当工作队列已满时,意味着系统的负载已经很高,此时创建新的线程可以提高系统的响应能力,尽快处理积压的任务。这种策略确保了在系统负载较高时,能够动态调整线程数以应对突发的需求。
关闭线程池
-
shutdownNow():立即关闭线程池(暴力),正在执行中的及队列中的任务会被中断,同时该方法会返回被中断的队列中的任务列表
-
shutdown():平滑关闭线程池,正在执行中的及队列中的任务能执行完成,后续进来的任务会被执行拒绝策略
-
isTerminated():当正在执行的任务及对列中的任务全部都执行(清空)完就会返回true
常用的一些线程池
newFixedThreadPool
定长线程池的线程数量达corePoolSize后,即使线程池没有可执行任务时,也不会释放线程。FixedThreadPool的工作队列为无界队列LinkedBlockingQueue(队列容量为Integer.MAX_VALUE), 这会导致以下问题:
- 线程池里的线程数量不超过corePoolSize,这导致了maximumPoolSize和keepAliveTime将会是个无用参数
- 由于使用了无界队列, 所以FixedThreadPool永远不会拒绝, 即饱和策略失效
newSingleThreadExecutor
单线程化的线程池初始化的线程池中只有一个线程,如果该线程异常结束,会重新创建一个新的线程继续执行任务,唯一的线程可以保证所提交任务的顺序执行.由于使用了无界队列, 所以SingleThreadPool永远不会拒绝, 即饱和策略失效
newCachedThreadPool
可缓存线程池的线程数可达到Integer.MAX_VALUE,即2147483647,内部使用SynchronousQueue作为阻塞队列; 和newFixedThreadPool创建的线程池不同,newCachedThreadPool在没有任务执行时,当线程的空闲时间超过keepAliveTime,会自动释放线程资源,当提交新任务时,如果没有空闲线程,则创建新线程执行任务,会导致一定的系统开销; 执行过程与前两种稍微不同:
- 主线程调用SynchronousQueue的offer()方法放入task, 倘若此时线程池中有空闲的线程尝试读取 SynchronousQueue的task, 即调用了SynchronousQueue的poll(), 那么主线程将该task交给空闲线程. 否则执行(2)
- 当线程池为空或者没有空闲的线程, 则创建新的线程执行任务.
- 执行完任务的线程倘若在60s内仍空闲, 则会被终止. 因此长时间空闲的CachedThreadPool不会持有任何线程资源.
newScheduledThreadPool
定时线程池,它可安排在给定延迟后运行命令或者定期地执行,注意一下使用的是延迟队列,弊端同newCachedThreadPool一致
newWorkStealingPool
工作窃取线程池,创建一个拥有多个任务队列的线程池,自己的活干完了去看看别人有没有没干完的活,如果有就拿过来帮他干,可以减少连接数,创建当前可用cpu数量的线程来并行执行,适用于大耗时的操作,可以并行来执行。
原理:
为每个工作者程分配一个双端队列(本地队列)用于存放需要执行的任务,当自己的队列没有数据的时候从其它工作者队列中获得一个任务继续执行
线程池不推荐使用Executors去创建,而是通过ThreadPoolExecutor的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。 说明:Executors各个方法的弊端:
- newFixedThreadPool和newSingleThreadExecutor: 主要问题是堆积的请求处理队列可能会耗费非常大的内存,甚至OOM。
- newCachedThreadPool和newScheduledThreadPool: 主要问题是线程数最大数是Integer.MAX_VALUE,可能会创建数量非常多的线程,甚至OOM。
三、线程池实现原理
其实java线程池的实现原理很简单,就是一个线程集合workerSet和一个阻塞队列workQueue。当用户向线程池提交一个任务(也就是线程)时,线程池会先将任务放入workQueue中。workerSet中的线程会不断的从workQueue中获取线程然后执行。当workQueue中没有任务的时候,worker就会阻塞,直到队列中有任务了就取出来继续执行。
Execute原理
当一个任务提交至线程池之后:
- 线程池首先当前运行的线程数量是否少于corePoolSize。如果是,则创建一个新的工作线程来执行任务。如果都在执行任务,则进入2.
- 判断BlockingQueue是否已经满了,倘若还没有满,则将线程放入BlockingQueue。否则进入3.
- 如果创建一个新的工作线程将使当前运行的线程数量超过maximumPoolSize,则交给RejectedExecutionHandler来处理任务。
当ThreadPoolExecutor创建新线程时,通过CAS来更新线程池的状态ctl.
四、Springboot中使用线程池
在Springboot中如何优雅的使用线程池。
|
|
|
|
其它相关方法
beforeExecute,afterExecute,这两个方法是protected修饰的,是留给开发人员去重写方法体实现自己的业务逻辑,非常适合做钩子函数,在任务run方法的前后增加业务逻辑,比如添加日志、统计等。
在线程池中execute()和submit()两个方法用于向线程池提交任务,
execute()方法没有返回值,它只能用于提交Runnable类型的任务。submit()方法不仅可以提交Runnable类型任务还可以提交Callable任务并且返回一个Future对象来获取任务的执行结果。另外execute执行任务时遇到异常会直接抛出,submit执行任务是遇到异常不会直接抛出,只有在使用Future的get方法获取返回值时才会抛出异常
文章作者 necor 上次更新 2024-07-03