贵州网站设计,邢台专业做网站推广,泰安建设网站,网站开发图片框文章内容已经收录在《面试进阶之路》#xff0c;从原理出发#xff0c;直击面试难点#xff0c;实现更高维度的降维打击#xff01;
目录 文章目录 目录Java线程池的核心内容详解线程池的优势什么场景下要用到线程池呢#xff1f;线程池中重要的参数【掌握】新加入一个任…文章内容已经收录在《面试进阶之路》从原理出发直击面试难点实现更高维度的降维打击
目录 文章目录 目录Java线程池的核心内容详解线程池的优势什么场景下要用到线程池呢线程池中重要的参数【掌握】新加入一个任务线程池如何进行处理呢【掌握】如何将任务提交到线程池中呢线程池是如何关闭的呢线程池为什么设计为任务队列满了才创建新线程线程池中线程异常后该线程会销毁吗 关于线程池在生产环境中的使用一个项目使用一个线程池还是多个线程池线程池在 RocketMQ 中的使用关于线程数量的设置美团技术团队针对线程池所做的优化自定义拒绝策略阿里手册中的线程池规范 Java线程池的核心内容详解
线程池的优势
首先线程池是将多个线程进行池化操作统一进行管理这样做有什么好处呢
降低创建、销毁线程的开销 线程池中维护固定数量的线程不需要临时进行线程的创建和销毁提高响应速度 对于新提交到线程池中的任务直接使用线程池中的空闲线程可以直接进行处理不需要等待创建线程节省资源可以重复利用线程
什么场景下要用到线程池呢
一般就是多 IO 的场景下需要用到像 IO 任务很多比如数据库操作、请求其他接口操作这都属于 IO 类任务IO 类任务的特点就是只需要线程去启动一下 IO 任务之后就等待 IO 结果返回即可IO 结果返回的时间是比较慢的 因此如果只使用单线程去执行 IO 任务的话由于这个等待时间比较长那么线程需要一直等待 IO 结果返回而无法执行其他操作
因此在多 IO 场景下可以使用线程池来加快 IO 任务的执行开启多个线程同时去启动多个 IO 任务可以加快 IO 任务的处理速度
线程池中重要的参数【掌握】
线程池中重要的参数如下
corePoolSize 核心线程数量maximumPoolSize 线程池最大线程数量 核心线程数非核心线程数keepAliveTime 非核心线程存活时间unit空闲线程存活时间单位keepAliveTime单位workQueue 工作队列任务队列存放等待执行的任务 LinkedBlockingQueue无界的阻塞队列最大长度为 Integer.MAX_VALUEArrayBlockingQueue基于数组的有界阻塞队列按FIFO排序SynchronousQueue同步队列不存储元素对于提交的任务如果有空闲线程则使用空闲线程来处理否则新建一个线程来处理任务PriorityBlockingQueue具有优先级的无界阻塞队列优先级通过参数Comparator实现。 threadFactory 线程工厂创建一个新线程时使用的工厂可以用来设定线程名、是否为daemon线程等等。handler 拒绝策略 有4种 AbortPolicy 直接抛出异常默认策略CallerRunsPolicy用调用者所在的线程来执行任务主线程执行DiscardOldestPolicy丢弃阻塞队列里最老的任务也就是队列里靠前的任务DiscardPolicy 当前任务直接丢弃
新加入一个任务线程池如何进行处理呢【掌握】
新加入一个任务线程池处理流程如下
如果核心线程数量未达到创建核心线程执行如果当前运行线程数量已经达到核心线程数量查看任务队列是否已满如果任务队列未满将任务放到任务队列如果任务队列已满看最大线程数是否达到如果未达到就新建非核心线程处理如果当前运行线程数量未达到最大线程数则创建非核心线程执行如果当前运行线程数量达到最大线程数根据拒绝策略处理 如何将任务提交到线程池中呢
有两种方式execute 和 submit
这两种方式的区别
execute execute 没有返回值execute 无法捕获任务过程中的异常 submit submit 会返回一个 Future 对象用来获取任务的执行结果submit 可以通过 Future 对象来捕获任务中的异常
execute 方式如下
ExecutorService executor Executors.newFixedThreadPool(5);
executor.execute(new Runnable() {public void run() {// 执行具体的任务逻辑System.out.println(Task executed using execute method);}
});
executor.shutdown();submit 方式如下
ExecutorService executor Executors.newFixedThreadPool(5);
FutureString future executor.submit(new CallableString() {public String call() {// 执行具体的任务逻辑return Task executed using submit method;}
});try {String result future.get(); // 获取任务执行结果System.out.println(result);
} catch (InterruptedException e) {// 处理中断异常
} catch (ExecutionException e) {// 处理任务执行异常
} finally {// 关闭线程池executor.shutdown();
}线程池是如何关闭的呢
通过调用线程池的 shutdown() 方法即可关闭线程池
调用之后会设置一个标志位表示当前线程池已经关闭会禁止向线程池中提交新的任务
去中断所有的空闲线程并且等待正在执行的任务执行完毕通过调用线程 interrupt() 方法当线程池中所有任务都执行完毕之后线程池就会被完全关闭
扩展thread.interrupt() 方法调用后线程会立即中断吗
不会调用 interrupt 只是将被中断线程的中断状态设置为 true通知被中断的线程自己处理中断而不是立即强制的让线程直接中断强制中断不安全
当外部调用线程进行中断的命令时如果该线程处于被阻塞的状态如 Thread.sleep()Object.wait()BlockingQueue#putBlockingQueue#take 等等时那么此时调用该线程的 interrupt 方法就会抛出 InterruptedException 异常
因此可以通过这个特点来优雅的停止线程在 《Java多线程核心技术》 一书中说到将 sleep() 和 interrupt() 搭配使用来停止线程
线程池为什么设计为任务队列满了才创建新线程
这里说一下在知乎上看到的一个问题个人觉得提问的比较好
线程池为什么设计为队列满核心线程数满了才创建新线程而不是队列积压一定阈值的时候创建新的线程
当队列积压满了之后创建非核心线程来执行任务只是一个 兜底措施
你想如果我们自己去设计一个线程池是不是只需要一个参数来管理线程池中的线程数量就可以了完全没必要去创建这些非核心线程执行任务
那么线程池的设计团队可不会考虑的这么简单它们不仅会考虑性能方面更是会保证比较高的 可用性
因为在 Java 应用中高并发 和 高可用 这两块都是比较重要的东西不仅要性能好还要不崩溃
就比如之前滴滴故障、阿里云故障、语雀故障所带来的影响都是比较大的对公司来讲整个可信度有所下降对于我们个人来讲可能有些人恰巧需要紧急使用但是由于发生故障不得已计划延期
所以线程池为了保证 高可用 就设计了任务队列以及在队列满了之后再去创建非核心线程处理溢出来的任务
当然任何设计都是平衡之后的选择如果你在公司项目需求与设计者的理念不符合可以基于原有设计做出封装来进行定制化操作
线程池中线程异常后该线程会销毁吗
向线程池中提交任务有 execute() 和 submit() 两种提交方式的区别如下 execute 执行任务execute 没有返回值无法捕获任务过程中的异常 submit 执行任务submit 会返回一个 Future 对象用来获取任务的执行结果可以通过 Future 对象来捕获任务中的异常
那么执行过程中发生异常线程会销毁吗
execute 无法捕捉任务过程中的异常是因为当任务在执行时遇到异常的话如果异常在线程执行过程中没有被捕获的话该异常就会导致线程停止执行并且在控制台打印异常之后该线程会终止线程池会创建一个新线程来替换他
submit 方式执行任务的话当执行过程中发生异常异常会被封装在 submit() 返回的 Future 对象中当调用 Future.get() 时可以捕获到 ExecutionException 异常因此使用 submit() 发生异常不会终止线程
参考线程池中线程异常后销毁还是复用
关于线程池在生产环境中的使用
这里整理了一些线程池在生产环境中使用的建议来帮助我们更好的在项目中使用线程池
一个项目使用一个线程池还是多个线程池
一般建议是不同的业务使用不同的线程池从而避免非核心业务对于核心业务的影响
如果所有的业务使用同一个线程池非核心业务可能执行速度很慢从而占用了很多线程迟迟不归还导致核心业务在任务队列中等待拿不到线程执行
并且还可能造成 死锁问题 当父子任务使用同一个线程池时父任务如果将核心线程全部占用之后等待子任务完成由于核心线程没有空闲的导致子任务进入到任务队列中等待线程资源导致父子任务之间互相等待
线程池在 RocketMQ 中的使用
在 MQ 中使用了很多线程池这里说一下在发送消息时使用的线程池
1、任务队列创建了 异步发送者线程池 任务队列 使用长度为 50000 的阻塞队列
2、线程数核心线程数 和 最大线程数 相同为 CPU 核数
3、存活时间非核心线程存活时间 60s
4、线程名称重写了线程工厂主要是 为了线程的命名规范 这样在查询日志时只要做好业务之间的隔离就可以很容易的根据线程名称来定位到对应的业务便于分析线上问题
private final ExecutorService defaultAsyncSenderExecutor;private final BlockingQueueRunnable asyncSenderThreadPoolQueue;this.asyncSenderThreadPoolQueue new LinkedBlockingQueueRunnable(50000);this.defaultAsyncSenderExecutor new ThreadPoolExecutor(Runtime.getRuntime().availableProcessors(),Runtime.getRuntime().availableProcessors(),1000 * 60,TimeUnit.MILLISECONDS,this.asyncSenderThreadPoolQueue,new ThreadFactory() {private AtomicInteger threadIndex new AtomicInteger(0);Overridepublic Thread newThread(Runnable r) {return new Thread(r, AsyncSenderExecutor_ this.threadIndex.incrementAndGet());}});那么我们在自己的项目中使用的线程池就可以参考 MQ 中的用法更加规范的使用线程池
至于为什么要这样设置核心线程数一方面是参考了设置核心线程数的经验CPU 密集型的任务令线程数等于 CPU 核心数减少了线程之间的上下文切换速度比较快另一方面 RocketMQ 肯定内部经过性能测试发现这样设置性能比较好一些
关于线程数量的设置
在项目中一般使用线程池的场景无非就两种
及时性任务 需要迅速完成降低用户等待时间非及时性任务 批量完成任务一般是后台任务
那么对于 及时性任务 来说需要尽可能快的完成任务因此要 尽可能增大可执行任务的线程数量 来尽可能快的完成任务不要设置任务队列 因为只有任务队列满了之后才会去创建非核心线程执行
对于 非及时性任务 来说这类任务并不面向用户特征是任务量很大需要批量处理不需要很低的延迟因此需要设置合适线程数量 利用有限的资源去尽可能快的执行任务 并且设置任务队列去缓冲任务但是尽量不要使用无界的任务队列无界队列任务堆积过多会造成 OOM
这里举一个线程池在高并发电商系统中的使用案例
这里我举一个使用线程池的真实生产环境的案例用户消息推送
对于中大型电商系统来说用户量一般最少都达到了千万级那么如果举办促销活动或者优惠活动了电商系统肯定需要给用户发送通知可能会有多个渠道发送比如短信、邮箱等等那么肯定是需要调用第三方平台的 API 了
调用其他平台 API毫无疑问就会产生网络 IO并且是 千万级别的网络 IO 如果只靠单线程去执行那可能等推送完之后促销活动也已经结束了
因此对于这种 IO 任务并且是大体量推送的 IO 任务就必须引入线程池来优化性能了通过多线程来进行任务的推送当然这里还使用了 RocketMQ 来进行解耦引入 MQ 之后就是使用线程池来生成大量消息推送到 MQ 中消费者再去订阅这些消息去调用第三方平台进行推送由于该文章主要是讲线程池的所以这里 MQ 的部分就简单说一下
ThreadPoolExecutor threadPoolExecutor new ThreadPoolExecutor(0,permits * 2,60,TimeUnit.SECONDS,new SynchronousQueue(),NamedDaemonThreadFactory.getInstance(name)
);这里也将线程池创建的代码给列出来这里顺带说一下线程池核心线程的参数为什么设置为 0
因为在消息推送这块并不是一直要推送的促销活动、发优惠券在正常情况下是不会推送发送消息的因此将核心线程数设置为 0 可以在没有推送任务的时候将线程池中的线程都回收掉有任务的时候再来创建非核心线程执行任务这样可以避免线程在没有任务时空闲占用资源
这里注意任务队列的选用
将核心线程数设置为 0 之后队列使用了 SynchronousQueue 因为这个队列是不存储元素的因此有任务来了就会创建非核心线程去执行
如果将设置了有容量的任务队列任务进来之后会先放在队列中并不会创建非核心线程
美团技术团队针对线程池所做的优化
在美团内部有多次因为线程池参数设置不合理而引发故障的案例
因此可以发现在不同场景下开发人员对参数的配置有一个大概的方向但是具体配置多少还没有一个通用的公式 导致上线之后线程池会因为 线程数设置过少 或者 任务队列设置不合理 而出现故障
因此美团技术团队设计了 动态化线程池 提供了对 线程池的监控 以及参数动态调整这样在调整参数之后通过监控可以看到整个线程池的负载情况可以选出比较合适的参数方案
那么这里重点的优化提升就在于两点
线程池参数的动态化设置线程池监控
这里提一下在线程监控中对线程池负载的定义
线程池的负载可以根据活跃的线程数和最大线程数的比值来反映
线程池活跃度 activeCount/maximumPoolSize 当活跃度升高代表着线程池负载在逐步上升
还可以 从任务队列中等待的任务数量 或者 发生拒绝策略的次数 来反映
总结一下
线程池参数的设置没有一个通用的公式要根据实际场景出发在设置之后可以对线程池的性能进行测试像对线程池进行性能测试的话就需要对线程池做监控来看在不同参数下线程池处理任务时的负载表现来设置更加合理的参数
自定义拒绝策略
在线程池中可以 自己去定义拒绝策略 如果线程池无法处理更多的任务了可以在自定义的拒绝策略中将拒绝的任务 异步持久化 到磁盘中去之后再通过一个后台线程去定时扫描这些被拒绝的任务慢慢执行
保证严格的任务不丢失如果线上机器突然宕机线程池的阻塞队列中的请求怎么办
如果宕机重启之后线程池阻塞队列中的任务就会全部丢失
如果想要解决这种情况的话有这么一个 解决方案 在将任务提交到线程池中去的时候先把任务在数据库中存储一份并记录任务执行的状态未提交、已提交、已完成执行完之后的话将任务状态标记为 已完成如果宕机后导致任务丢失就可以去数据库中扫描任务重新提交给线程池执行
阿里手册中的线程池规范
在使用线程池的时候需要注意一些规范以免出现不必要的问题可以参考阿里巴巴 Java 开发手册如下
线程池名称命名规范 线程池创建规范