Java线程池详解
本⽂包含知识点
线程池的使⽤场景分析
线程池的创建及重要参数
线程池实现线程复⽤的原理
springboot中使⽤线程池
Callabel与Runnable任务
在基于spring体系的业务中正确地关闭线程池
实现优先使⽤运⾏线程及调整线程数⼤⼩的线程池(线程池的优化)
在java web项⽬中慎⽤Executors以及⾮守护线程
1.线程池使⽤场景?
java中经常需要⽤到多线程来处理⼀些业务,我们⾮常不建议单纯使⽤继承Thread或者实现Runnable接⼝的⽅式来创建线程,那样势必有创建及销毁线程耗费资源、线程上下⽂切换问题。同时创建过多的线程也可能引发资源耗尽的风险,这个时候引⼊线程池⽐较合理,⽅便线程任务的管理。java中涉及到线程池的相关类均在jdk1.5开始的urrent包中,涉及到的⼏个核⼼类及接⼝包括:Executor、Executors、ExecutorService、ThreadPoolExecutor、FutureTask、Callable、Runnable等。
加快请求响应(响应时间优先)
⽐如⽤户在饿了么上查看某商家外卖,需要聚合商品库存、店家、价格、红包优惠等等信息返回给⽤户,接⼝逻辑涉及到聚合、级联等查询,从这个⾓度来看接⼝返回越快越好,那么就可以使⽤多线程⽅式,把聚合/级联查询等任务采⽤并⾏⽅式执⾏,从⽽缩短接⼝响应时间。这种场景下使⽤线程池的⽬的就是为了缩短响应时间,往往不去设置队列去缓冲并发的请求,⽽是会适当调⾼corePoolSize和maxPoolSize去尽可能的创造线程来执⾏任务。
加快处理⼤任务(吞吐量优先)
⽐如业务中台每10分钟就调⽤接⼝统计每个系统/项⽬的PV/UV等指标然后写⼊多个sheet页中返回,这种情况下往往也会使⽤多线程⽅式来并⾏统计。和"时间优先"场景不同,这种场景的关注点不在于
尽可能快的返回,⽽是关注利⽤有限的资源尽可能的在单位时间内处理更多的任务,即吞吐量优先。这种场景下我们往往会设置队列来缓冲并发任务,并且设置合理的corePoolSize和maxPoolSize参数,这个时候如果设置了太⼤的corePoolSize和maxPoolSize可能还会因为线程上下⽂频繁切换降低任务处理速度,从⽽导致吞吐量降低。
以上两种使⽤场景和JVM⾥的ParallelScavenge和CMS垃圾收集器有较⼤的类⽐性,ParallelScavenge垃圾收集器关注点在于达到可观的吞吐量,⽽CMS垃圾收集器重点关注尽可能缩短GC停顿时间。
本⼈项⽬中使⽤线程池的⼀个需求场景
2.线程池的创建及重要参数
线程池可以⾃动创建也可以⼿动创建,⾃动创建体现在Executors⼯具类中,常见的可以创建newFixedThreadPool、newCachedThreadPool、newSingleThreadExecutor、newScheduledThreadPool;⼿动创建体现在可以灵活设置线程池的各个参
数,体现在代码中即ThreadPoolExecutor类构造器上各个实参的不同:
public static ExecutorService newFixedThreadPool(int var0) {
return new ThreadPoolExecutor(var0, var0, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue());
}
public static ExecutorService newSingleThreadExecutor() {
return new Executors.FinalizableDelegatedExecutorService(new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue()));  }
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, 2147483647, 60L, TimeUnit.SECONDS, new SynchronousQueue());
}
public static ScheduledExecutorService newScheduledThreadPool(int var0) {
return new ScheduledThreadPoolExecutor(var0);
}
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {……}
ThreadPoolExecutor中重要的⼏个参数详解
corePoolSize:核⼼线程数,也是线程池中常驻的线程数,线程池初始化时默认是没有线程的,当任务来临时才开始创建线程去执⾏任务
maximumPoolSize:最⼤线程数,在核⼼线程数的基础上可能会额外增加⼀些⾮核⼼线程,需要注意的是只有当workQueue队列填满时才会创建多于corePoolSize的线程(线程池总线程数不超过maxPoolSize)
keepAliveTime:⾮核⼼线程的空闲时间超过keepAliveTime就会被⾃动终⽌回收掉,注意当corePoolSize=maxPoolSize
时,keepAliveTime参数也就不起作⽤了(因为不存在⾮核⼼线程);
unit:keepAliveTime的时间单位
workQueue:⽤于保存任务的队列,可以为⽆界、有界、同步移交三种队列类型之⼀,当池⼦⾥的⼯作线程数⼤于corePoolSize时,这时新进来的任务会被放到队列中
threadFactory:创建线程的⼯⼚类,默认使⽤Executors.defaultThreadFactory(),也可以使⽤guava库的ThreadFactoryBuilder 来创建
handler:线程池⽆法继续接收任务(队列已满且线程数达到maximunPoolSize)时的饱和策略,取值有AbortPolicy、
CallerRunsPolicy、DiscardOldestPolicy、DiscardPolicy
线程池中的线程创建流程图:
举个栗⼦:现有⼀个线程池,corePoolSize=10,maxPoolSize=20,队列长度为100,那么当任务过来会先创建10个核⼼线程数,接下来进来的任务会进⼊到队列中直到队列满了,会创建额外的线程来执⾏任务(最多20个线程),这个时候如果再来任务就会执⾏拒绝策略。
workQueue队列
SynchronousQueue(同步移交队列):队列不作为任务的缓冲⽅式,可以简单理解为队列长度为零
LinkedBlockingQueue(⽆界队列):队列长度不受限制,当请求越来越多时(任务处理速度跟不上任务提交速度造成请求堆积)可能导致内存占⽤过多或OOM
ArrayBlockintQueue(有界队列):队列长度受限,当队列满了就需要创建多余的线程来执⾏任务
常见的⼏种⾃动创建线程池⽅式
⾃动创建线程池的⼏种⽅式都封装在Executors⼯具类中:
newFixedThreadPool:使⽤的构造⽅式为new ThreadPoolExecutor(var0, var0, 0L, TimeUnit.MILLISECONDS, new
LinkedBlockingQueue()),设置了corePoolSize=maxPoolSize,keepAliveTime=0(此时该参数没作⽤),⽆界队列,任务可以⽆限放⼊,当请求过多时(任务处理速度跟不上任务提交速度造成请求堆积)可能导致占⽤过多内存或直接导致OOM异常
newSingleThreadExector:使⽤的构造⽅式为new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new
LinkedBlockingQueue(), var0),基本同newFixedThreadPool,但是将线程数设置为了1,单线程,弊端和newFixedThreadPool ⼀致
newCachedThreadPool:使⽤的构造⽅式为new ThreadPoolExecutor(0, 2147483647, 60L, TimeUnit.SECONDS, new SynchronousQueue()),corePoolSize=0,maxPoolSize为很⼤的数,同步移交队列,也就是说不维护常驻线程(核⼼线程),每次来请求直接创建新线程来处理任务,也不使⽤队列缓冲,会⾃动回收多余线程,由于将maxPoolSize设置成Integer.MAX_VALUE,当请求很多时就可能创建过多的线程,导致资源耗尽OOM
newScheduledThreadPool:使⽤的构造⽅式为new ThreadPoolExecutor(var1, 2147483647, 0L,
TimeUnit.NANOSECONDS, new ScheduledThreadPoolExecutor.DelayedWorkQueue()),⽀持定时周期性执⾏,注意⼀下使⽤的是延迟队列,弊端同newCachedThreadPool⼀致
所以根据上⾯分析我们可以看到,FixedThreadPool和SigleThreadExecutor中之所以⽤LinkedBlockingQueue⽆界队列,是因为设置了corePoolSize=maxPoolSize,线程数⽆法动态扩展,于是就设置了⽆界阻塞队列来应对不可知的任务量;⽽CachedThreadPool则使⽤的是SynchronousQueue同步移交队列,为什么使⽤这个队列呢?因为CachedThreadPool设置了
corePoolSize=0,maxPoolSize=Integer.MAX_VALUE,来⼀个任务就创建⼀个线程来执⾏任务,⽤不到队列来存储任务;SchduledThreadPool⽤的是延迟队列DelayedWorkQueue。在实际项⽬开发中也是推荐使⽤⼿动创建线程池的⽅式,⽽不⽤默认⽅式,关于这点在《阿⾥巴巴开发规范》中是这样描述的:
handler拒绝策略
AbortPolicy:中断抛出异常
DiscardPolicy:默默丢弃任务,不进⾏任何通知
DiscardOldestPolicy:丢弃掉在队列中存在时间最久的任务
CallerRunsPolicy:让提交任务的线程去执⾏任务(对⽐前三种⽐较友好⼀丢丢)
关闭线程池
shutdownNow():⽴即关闭线程池(暴⼒),正在执⾏中的及队列中的任务会被中断,同时该⽅法会返回被中断的队列中的任务列表shutdown():平滑关闭线程池,正在执⾏中的及队列中的任务能执⾏完成,后续进来的任务会被执⾏拒绝策略
isTerminated():当正在执⾏的任务及对列中的任务全部都执⾏(清空)完就会返回true
3.线程池实现线程复⽤的原理
1.线程池⾥执⾏的是任务,核⼼逻辑在ThreadPoolExecutor类的execute⽅法中,同时ThreadPoolExecutor中维护了
HashSet<Worker> workers;
2.addWorker()⽅法来创建线程执⾏任务,如果是核⼼线程的任务,会赋值给Worker的firstTask属性;
3.Worker实现了Runnable,本质上也是任务,核⼼在run()⽅法⾥;
java线程池创建的四种4.run()⽅法的执⾏核⼼runWorker(),⾃旋拿任务while (task != null || (task = getTask()) != null)),task是核⼼线程Worker的firstTask或者getTask();
1.若当前⼯作线程数量⼤于核⼼线程数->说明此线程是⾮核⼼⼯作线程,通过poll()拿任务,未拿到任务即getTask()返回null,然后会在processWorkerExit(w, completedAbruptly)⽅法释放掉这个⾮核⼼⼯作线程的引⽤;
2.若当前⼯作线程数量⼩于核⼼线程数->说明此时线程是核⼼⼯作线程,通过take()拿任务
3.take()⽅式取任务,如果队列中没有任务了会调⽤await()阻塞当前线程,直到新任务到来,所以核⼼⼯作线程不会被回收; 当执⾏execute⽅法⾥的workQueue.offer(command)时会调⽤Condition.singal()⽅法唤醒⼀个之前阻塞的线程,这样核⼼线程即可复⽤
⼿动创建线程池(推荐)
那么上⾯说了使⽤Executors⼯具类创建的线程池有隐患,那如何使⽤才能避免这个隐患呢?对症下药,建⽴⾃⼰的线程⼯⼚类,灵活设置
关键参数:
//这⾥默认拒绝策略为AbortPolicy
private static ExecutorService executor = new ThreadPoolExecutor(10,10,60L, TimeUnit.SECONDS,new ArrayBlockingQueue(10));
使⽤guava包中的ThreadFactoryBuilder⼯⼚类来构造线程池:
private static ThreadFactory threadFactory = new ThreadFactoryBuilder().build();
private static ExecutorService executorService = new ThreadPoolExecutor(10, 10, 60L, TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(10), threadFa 通过guava的ThreadFactory⼯⼚类还可以指定线程组名称,这对于后期定位错误时也是很有帮助的
ThreadFactory threadFactory = new ThreadFactoryBuilder().setNameFormat("thread-pool-d%").build();
4.Springboot中使⽤线程池
springboot可以说是⾮常流⾏了,下⾯说说如何在springboot中优雅的使⽤线程池
/**
* @ClassName ThreadPoolConfig
* @Description 配置类中构建线程池实例,⽅便调⽤
* @Author simonsfan
* @Date 2018/12/20
* Version  1.0
*/
@Configuration
public class ThreadPoolConfig {
@Bean(value = "threadPoolInstance")
public ExecutorService createThreadPoolInstance() {
//通过guava类库的ThreadFactoryBuilder来实现线程⼯⼚类并设置线程名称
ThreadFactory threadFactory = new ThreadFactoryBuilder().setNameFormat("thread-pool-%d").build();
ExecutorService threadPool = new ThreadPoolExecutor(10, 16, 60L, TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(100), threadFactory, new T        return threadPool;
}
}

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。