java线程池介绍以及使用

实现目标

能够理解线程池"是什么","为什么"以及能够在合适的场景里去使用线程池

目录

一、线程池是什么?

二、为什么要使用线程池

1.使用线程池可以重复利用已有的线程继续执行任务,避免线程在创建销毁时造成的消耗

2.由于没有线程创建和销毁的消耗,可以提高系统的响应速度

3.通过线程可以对线程进行合理的管理,根据系统的承受能力调整可运行线程数量的大小等等(可以自己设计一个监控将线程的参数合理的管理起来)

三、线程池的使用场景(如何合理的去使用)

四、线程池的使用

 1.java官方提供了哪几种线程池?

2.线程池的七大参数

3.线程池的使用原理

以下的例子更好的理解

4.CompletableFuture

 五、线程池的回收

六、实际运用

总结

一、线程池是什么?

是一种线程使用模式。线程过多会带来调度开销,进而影响缓存局部性和整体性能。而线程池维护着多个线程,等待着监督管理者分配可并发执行的任务。这避免了在处理段时间任务时创建与销毁线程的代价。线程池不仅能保证内核的充分利用,还能防止过度调度。可用线程数量应该取决于可用的并发处理器、处理器内核、内核、网络sockets等的数量

按照线程池执行任务分类 线程池可以分为 cpu密集型任务和io密集型任务

cpu密集型任务,需要线程长时间进行的复杂的运算,这种类型的任务需要少创建线程,躲过的线程将会频繁引起上下文切换,降低任务处理速度

io密集型任务,由于线程并不是一直在运行,可能大部分时间在等待io读取/写入数据,增加线程数量可以提高并发度,尽可能多处理任务

二、为什么要使用线程池

1.使用线程池可以重复利用已有的线程继续执行任务,避免线程在创建销毁时造成的消耗
2.由于没有线程创建和销毁的消耗,可以提高系统的响应速度
3.通过线程可以对线程进行合理的管理,根据系统的承受能力调整可运行线程数量的大小等等(可以自己设计一个监控将线程的参数合理的管理起来)

三、线程池的使用场景(如何合理的去使用)

1. 快速响应用户请求,响应速度优先
    (1) 比如一个用户请求,需要通过 RPC 调用好几个服务去获取数据然后聚合返回,此场景就可以用线程池并行调用,响应时间取决于响应最慢的那个 RPC 接口的耗时。

    (2) 某些批量操作,用户请求批量删除10个账号,希望能够快速得到响应,如果删除一个账号需要1秒,删除10个就需要10秒。如果你用了线程池异步执行,显然会很快得到响应结果。具体多快,取决于你服务器的性能和线程池的参数配置。

    (3) 或者一个注册请求,注册完之后要发送短信、邮件通知,为了快速返回给用户,可以将该通知操作丢到线程池里异步去执行,然后直接返回客户端成功,提高用户体验。

这样的场景就建议不设置队列或设置短的队列去缓冲并发任务,调高corePoolSize和maxPoolSize去尽可能创造多的线程快速执行任务。

2. 单位时间处理更多请求,吞吐量优先
    比如接受 MQ 消息,然后去调用第三方接口查询数据,此场景并不追求快速响应,主要利用有限的资源在单位时间内尽可能多的处理任务,可以利用队列进行任务的缓冲

这类场景任务量巨大,并不需要瞬时的完成,而是关注如何使用有限的资源,尽可能在单位时间内处理更多的任务。所以应该设置队列去缓冲并发任务,调整合适的corePoolSize去设置处理任务的线程数。在这里,设置的线程数过多可能还会引发线程上下文切换频繁的问题,也会降低处理任务的速度,降低吞吐量。
 

四、线程池的使用

 1.java官方提供了哪几种线程池?

JDK 中提供了 5 中不同线程池的创建方式,下面我分别说一下每一种线程 池以及它的特点。

newCachedThread Pool  是一种可以缓存的线程池,它可以用来处理大量短期 的突发流量。

它的特点有三个,最大线程数是 Integer.MaxValue ,线程存活时间是 60 秒,阻 塞队列用的是 SynchronousQueue,这是一种不存才任何元素的阻塞队列,也就 是每提交一个任务给到线程池,都会分配一个工作线程来处理,由于最大线程数 没有限制。

所以它可以处理大量的任务,另外每个工作线程又可以存活 60s,使得这些工作 线程可以缓存起来应对更多任务的处理。

newFixedThread Pool ,是一种固定线程数量的线程池。

它的特点是核心线程和最大线程数量都是一个固定的值

如果任务比较多工作线程处理不过来,就会加入到阻塞队列里面等待。

newSingleThread Executor,只有一个工作线程的线程池。

并且线程数量无法动态更改,因此可以保证所有的任务都按照 FIFO 的方式顺序 执行。

newScheduledThread Pool ,具有延迟执行功能的线程池

可以用它来实现定时调度

newWorkStealingPoolJava8 里面新加入的一个线程池

它内部会构建一个 ForkJoinPool,利用工作窃取的算法并行处理请求。

这些线程都是通过工具类 Executors 来构建  ,线程 的最终 实现类  Thread PoolExecutor

2.线程池的七大参数

corePoolSize:核心线程池的大小

maximumPoolSize:线程池能创建线程的最大个数

keepAliveTime:空闲线程存活时间

unit:时间单位,为keepAliveTime指定时间单位

workQueue:阻塞队列,用于保存任务的阻塞队列

threadFactory:创建线程的工程类

handler:饱和策略(拒绝策略)

3.线程池的使用原理

java线程池介绍以及使用_第1张图片

线程池的工作过程如下:

当一个任务提交至线程池之后, 

1. 线程池首先判断核心线程池里的线程是否已经满了。如果不是,则创建一个新的工作线程来执行任务。否则进入2. 

2. 判断工作队列是否已经满了,倘若还没有满,将线程放入工作队列。否则进入3. 

3. 判断线程池里的线程是否都在执行任务。如果不是,则创建一个新的工作线程来执行。如果线程池满了,则交给饱和策略来处理任务。

以下的例子更好的理解

. 核心线程比作公司正式员工

. 非核心线程比作外包员工

. 阻塞队列比作需求池

. 提交任务比作提需求

java线程池介绍以及使用_第2张图片

. 当产品提个需求 ,正式员工(核心线程)先需求(执行任务)

. 如果正式员工都有需求在做 ,即核心线程数已满 产品就需求先放需求池(阻 塞队列)。

. 如果需求池(阻塞队列)也满了 ,但这时候产品继续提需求,怎么办呢?那就请外包 (非核心线程)来做。

. 如果所有员工(最大线程数也满了)都有需求在做了 ,那就执行拒绝策略。

. 如果外包员工需求做完了 ,它经过一段(keepAliveTime)空闲时间 ,就离开 公司了。

4.CompletableFuture

CompletableFuture  JDK1.8 里面引入的一个基于事件驱动的异步回调类。

简单来说,就是当使用异步线程去执行一个任务的时候,我们希望在任务结束以 后触发一个后续的动作。

 CompletableFuture 就可以实现这个功能。

举个简单的例子,比如在一个批量支付的业务逻辑里面,涉及到查询订单、支付、 发送邮件通知这三个逻辑。

这三个逻辑是按照顺序同步去实现的,也就是先查询到订单以后,再针对这个订 单发起支付,支付成功以后再发送邮件通知。

而这种设计方式导致这个方法的执行性能比较慢。

java线程池介绍以及使用_第3张图片

所以,这里可以直接使用 CompletableFuture ,也就是说把查询订单的逻辑放在 一个异步线程池里面去处理。

然后基于 CompletableFuture 的事件回调机制的特性,可以配置查询订单结束后 自动触发支付,支付结束后自动触发邮件通知。

从而极大的提升这个这个业务场景的处理性能!

java线程池介绍以及使用_第4张图片

CompletableFuture 提供了 5 种不同的方式,把多个异步任务组成一个具有先后 关系的处理链,然后基于事件驱动任务链的执行。

第一种,thenCombine,把两个任务组合在一起,当两个任务都执行结束以后触 发事件回调。 

第二种,thenCompose ,把两个任务组合在一起,这两个任务串行执行,也就 是第一个任务执行完以后自动触发执行第二个任务。 

第三种,thenAccept,第一个任务执行结束后触发第二个任务,并且第一个任务 的执行结果作为第二个任务的参数,这个方法是纯粹接受上一个任务的结果,不 返回新的计算值。

第四种,thenApply ,和 thenAccept 一样,但是它有返回值。

第五种,thenRun ,就是第一个任务执行完成后触发执行一个实现了 Runnable 接口的任务。

最后,我认为,CompletableFuture 弥补了原本 Future 的不足,使得程序可以 在非阻塞的状态下完成异步的回调机制。

 五、线程池的回收

首先,线程池里面分为核心线程和非核心线程。

核心线程是常驻在线程池里面的工作线程,它有两种方式初始化。

向线程池里面添加任务的时候,被动初始

主动调用 prestartAllCoreThreads方法

当线程池里面的队列满了的情况下,为了增加线程池的任务处理能力。

线程池会增加非核心线程。

核心线程和非核心线程的数量,是在构造线程池的时候设置的,也可以动态进行 更改。

由于非核心线程是为了解决任务过多的时候临时增加的,所以当任务处理完成后,

工作线程处于空闲状态的时候,就需要回收。

因为所有工作线程都是从阻塞队列中去获取要执行的任务,所以只要在一定时间 内,阻塞队列没有任何可以处理的任务,那这个线程就可以结束了。

这个功能是通过阻塞队列里面的 poll 方法来完成的。这个方法提供了超时时间和 超时时间单位这两个参数当超过指定时间没有获取到任务的时候,poll 方法返回 null ,从而终止当前线程完成线程回收。

默认情况下,线程池只会回收非核心线程,如果希望核心线程也要回收,可以

设置 allowCoreThreadTimeOut 这个属性为 true,一般情况下我们不会去回收核 心线程。

因为线程池本身就是实现线程的复用,而且这些核心线程在没有任务要处理的时 候是处于阻塞状态并没有占用 CPU 资源。

六、实际运用


说之前,先来说说Java提供的三种创建线程池的工具(注意:自己做Demo可用,做项目咱不用)

1. Executors.newScheduledThreadPool();

2. Executors.newWorkStealingPool();

3. Executors.newFixedThreadPool();

阿里编码规约曾说:

【强制】线程池不允许使用Executors去创建, 而是通过ThreadPoolExecutor的方式, 这样的处理方式让写的同学更加明确线程池的运行规则, 规避资源耗尽的风险。

说明:Executors返回的线程池对象的弊端如下:

1)FixedThreadPool和SingleThreadPool:允许的请求队列长度为Integer.MAX_VALUE,可能会堆积大量的请求,从而导致OOM。

2)CachedThreadPool:允许的创建线程数量为Integer.MAX_VALUE,可能会创建大量的线程,从而导致OOM。

3)ScheduledThreadPool:允许的请求队列长度为Integer.MAX_VALUE,可能会堆积大量的请求,从而导致OOM。

我的天这要是上线了,搞不好年终奖与你擦肩而过。
 

总结

本文借鉴

Java线程池介绍及使用(这一篇就够了)_java 线程池使用-CSDN博客

Java线程池实现原理及其在美团业务中的实践 (推荐观看)

你可能感兴趣的:(java,开发语言)