5.5线程池使用技巧介绍
目录介绍
- 01.快速了解线程池
- 1.1 学习本节课目标
- 1.2 开发中遇到的问题
- 1.3 创建线程消耗什么资源
- 1.4 为什么用多线程
- 1.5 多线程应用场景
- 1.6 线程资源利用率
- 1.7 创建多少线程合适
- 1.8 为何引入线程池
- 02.线程池设计架构
- 2.1 线程池设计架构
- 2.2 常见类有那些
- 2.6 线程池中线程创建规则
- 2.7 理解线程优化级
- 2.8 线程池整体架构图
- 03.ThreadPoolExecutor
- 3.1 ThreadPoolExecutor参数
- 3.2 ThreadPoolExecutor使用
- 3.3 四种线程池类用途
- 04.线程池实践使用说明
- 4.1 newFixedThreadPool
- 4.2 newCachedThreadPool
- 4.3 newScheduledThreadPool
- 4.4 newSingleThreadExecutor
- 4.5 newWorkStealingPool
- 4.6 线程池该如何选择
- 4.7 如何给线程赋予名字
- 4.8 线程池如何处理线程异常
- 4.9 线程池分析工具实践
- 05.线程池常用方法原理
- 5.0 线程池整体流程图
- 5.1 线程池构造流程
- 5.2 线程池初始化和容量
- 5.3 线程池execute
- 5.4 获取线程池状态
- 5.5 线程池任务队列原理
- 5.6 线程池拒绝策略原理
- 06.线程池实践的总结
- 6.1 执行流程介绍
- 6.2 线程池的使用技巧
- 6.3 线程池实践问题总结
- 6.4 线程池大小选择策略
- 6.5 项目中用到多个线程池
01.快速了解线程池
1.1 学习本节课目标
1.2 开发中遇到的问题
- 在我们的开发中经常会使用到多线程。
- 例如在Android中,由于主线程的诸多限制,像网络请求等一些耗时的操作我们必须在子线程中运行。
- 为何使用线程池
- 我们往往会通过new Thread来开启一个子线程,待子线程操作完成以后通过Handler切换到主线程中运行。
- 这么以来我们无法管理我们所创建的子线程,并且无限制的创建子线程,它们相互之间竞争,很有可能由于占用过多资源而导致死机或者OOM。
- 所以在Java中为我们提供了线程池来管理我们所创建的线程。
- 线程是不能够重复启动的,创建或销毁线程存在一定的开销,所以利用线程池技术来提高系统资源利用效率,并简化线程管理,已经是非常成熟的选择。
1.3 创建线程消耗什么资源
- 实际上创建线程远不是创建一个对象那么简单
- 创建对象,仅仅是在 JVM 的堆里分配一块内存而已;而创建一个线程,却需要调用操作系统内核的 API,然后操作系统要为线程分配一系列的资源,这个成本就很高,所以线程是一个重量级的对象,应该避免频繁创建和销毁。
1.4 为什么用多线程
- 使用多线程,本质上就是提升程序性能。
- 不过此刻谈到的性能,使用多线程会快,效率高,这种无法度量的感性认识很不科学,所以在提升性能之前,首要问题是:如何度量性能。
- 度量性能的指标有很多,但是有两个指标是最核心的,它们就是延迟和吞吐量。
- 延迟指的是发出请求到收到响应这个过程的时间;延迟越短,意味着程序执行得越快,性能也就越好。
- 吞吐量指的是在单位时间内能处理请求的数量;吞吐量越大,意味着程序能处理的请求越多,性能也就越好。
- 这两个指标内部有一定的联系(同等条件下,延迟越短,吞吐量越大),但是由于它们隶属不同的维度(一个是时间维度,一个是空间维度),并不能互相转换。
1.5 多线程应用场景
- 所谓提升性能,从度量的角度,主要是降低延迟,提高吞吐量。
- 这也是我们使用多线程的主要目的。那我们该怎么降低延迟,提高吞吐量呢?这个就要从多线程的应用场景说起了。
- 要想“降低延迟,提高吞吐量”,对应的方法呢,基本上有两个方向,一个方向是优化算法,另一个方向是将硬件的性能发挥到极致。
- 前者属于算法范畴,后者则是和并发编程息息相关了。那计算机主要有哪些硬件呢?主要是两类:一个是 I/O,一个是 CPU。
- 简言之,在并发编程领域,提升性能本质上就是提升硬件的利用率,再具体点来说,就是提升 I/O 的利用率和 CPU 的利用率。
- 有个疑问,操作系统不是已经解决了硬件的利用率问题了吗?
- 例如操作系统已经解决了磁盘和网卡的利用率问题,利用中断机制还能避免 CPU 轮询 I/O 状态,也提升了 CPU 的利用率。
- 但是操作系统解决硬件利用率问题的对象往往是单一的硬件设备,而我们的并发程序,往往需要 CPU 和 I/O 设备相互配合工作,也就是说,我们需要解决 CPU 和 I/O 设备综合利用率的问题。
- 关于这个综合利用率的问题,操作系统虽然没有办法完美解决,但是却给我们提供了方案,那就是:多线程。
1.6 线程资源利用率
- 用一个简单的示例来说明:如何利用多线程来提升 CPU 和 I/O 设备的利用率?
- 假设程序按照 CPU 计算和 I/O 操作交叉执行的方式运行,而且 CPU 计算和 I/O 操作的耗时是 1:1。
- 用一些实际案例来分析利用率
- 案例1: 单核,如果只有一个线程,执行 CPU 计算的时候,I/O 设备空闲;执行 I/O 操作的时候,CPU 空闲,所以 CPU 的利用率和 I/O 设备的利用率都是 50%。
- 案例2: 单核,如果有两个线程,当线程 A 执行 CPU 计算的时候,线程 B 执行 I/O 操作;当线程 A 执行 I/O 操作的时候,线程 B 执行 CPU 计算,这样 CPU 的利用率和 I/O 设备的利用率就都达到了 100%。
- 案例3: 多核,如果在 4 核的 CPU 上利用 4 个线程执行,线程 A 计算[1,25 亿),线程 B 计算[25 亿,50 亿),线程 C 计算[50,75 亿),线程 D 计算[75 亿,100 亿],之后汇总,那么理论上应该比一个线程计算[1,100 亿]快将近 4 倍,响应时间能够降到 25%。一个线程,对于 4 核的 CPU,CPU 的利用率只有 25%,而 4 个线程,则能够将 CPU 的利用率提高到 100%。
- 案例2,将 CPU 的利用率和 I/O 设备的利用率都提升到了 100%,会对性能产生了哪些影响呢?
- 容易看出:单位时间处理的请求数量翻了一番,也就是说吞吐量提高了 1 倍。如果 CPU 和 I/O 设备的利用率都很低,那么可以尝试通过增加线程来提高吞吐量。
1.7 创建多少线程合适
- 创建多少线程合适,要看多线程具体的应用场景。
- 程序一般都是 CPU 计算和 I/O 操作交叉执行的,I/O 设备的速度相对于 CPU 来说都很慢。
- 大部分情况下,I/O 操作执行的时间相对于 CPU 计算来说都非常长,这种场景我们一般都称为 I/O 密集型计算; CPU 密集型计算大部分场景下都是纯 CPU 计算。
- I/O 密集型程序和 CPU 密集型程序,计算最佳线程数的方法是不同的。
- 对于 CPU 密集型计算,多线程本质上是提升多核 CPU 的利用率
- 所以对于一个 4 核的 CPU,每个核一个线程,理论上创建 4 个线程就可以了,再多创建线程也只是增加线程切换的成本。
- 所以,对于 CPU 密集型的计算场景,理论上“线程的数量 =CPU 核数”就是最合适的。
- 不过在工程上,线程的数量一般会设置为“CPU 核数 +1”,这样的话,当线程因为偶尔的内存页失效或其他原因导致阻塞时,这个额外的线程可以顶上,从而保证 CPU 的利用率。
- 对于 I/O 密集型的计算场景
- 如果 CPU 计算和 I/O 操作的耗时是 1:1,那么 2 个线程是最合适的。如果 CPU 计算和 I/O 操作的耗时是 1:2,那多少个线程合适呢?
- 是 3 个线程,如下图所示:CPU 在 A、B、C 三个线程之间切换,对于线程 A,当 CPU 从 B、C 切换回来时,线程 A 正好执行完 I/O 操作。这样 CPU 和 I/O 设备的利用率都达到了 100%。
image
1.8 为何引入线程池
- Java引入线程池的主要目的是为了更有效地管理和复用线程资源,以提高多线程应用程序的性能和可伸缩性。
- 1.降低线程创建和销毁的开销:线程池通过预先创建一组线程,并将它们保持在池中,可以避免频繁的线程创建和销毁,从而降低开销。
- 2.提高线程的复用性:线程池中的线程可以被重复使用来执行多个任务。当一个任务完成后,线程可以立即被分配给下一个任务,而不需要等待新线程的创建。
- 3.控制并发线程数量:线程池可以限制并发执行的线程数量,防止系统过载。
- 4.提供任务队列和调度机制:线程池通常包含一个任务队列,用于存储待执行的任务。当线程池中的线程空闲时,它们可以从任务队列中获取任务并执行。
- 5.提供线程管理和监控功能:线程池提供了一些管理和监控线程的功能,例如线程的状态、执行时间、完成情况等。
02.线程池核心类
2.1 线程池设计架构
2.2 常见类有那些
2.6 线程池中线程创建规则
- ThreadPoolExecutor对象初始化时,不创建任何执行线程,当有新任务进来时,才会创建执行线程。构造ThreadPoolExecutor对象时,需要配置该对象的核心线程池大小和最大线程池大小
- 1.当目前执行线程的总数小于核心线程大小时,所有新加入的任务,都在新线程中处理。
- 2.当目前执行线程的总数大于或等于核心线程时,所有新加入的任务,都放入任务缓存队列中。
- 3.当目前执行线程的总数大于或等于核心线程,并且缓存队列已满,同时此时线程总数小于线程池的最大大小,那么创建新线程,加入线程池中,协助处理新的任务。
- 4.当所有线程都在执行,线程池大小已经达到上限,并且缓存队列已满时,就rejectHandler拒绝新的任务。
2.7 理解线程优化级
- 线程池遵循的规则
- 当线程池中的核心线程数量未达到最大线程数时,启动一个核心线程去执行任务;
- 如果线程池中的核心线程数量达到最大线程数时,那么任务会被插入到任务队列中排队等待执行;
- 如果在上一步骤中任务队列已满但是线程池中线程数量未达到限定线程总数,那么启动一个非核心线程来处理任务;
- 如果上一步骤中线程数量达到了限定线程总量,那么线程池则拒绝执行该任务,且ThreadPoolExecutor会调用RejectedtionHandler的rejectedExecution方法来通知调用者。
- 优先级
- 关键点在于:核心线程数、最大线程数和任务队列数,执行流程如下,记住一点,优先级:核心线程数 > 任务队列数 > 最大线程数。
2.8 线程池整体架构图
03.ThreadPoolExecutor
3.1 ThreadPoolExecutor参数
- 可以通过ThreadPoolExecutor来创建一个线程池。
ExecutorService service = new ThreadPoolExecutor(....);
- 下面我们就来看一下ThreadPoolExecutor中的一个构造方法。方法中的参数含义下面依次说明:
public ThreadPoolExecutor( int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler)
- 1.corePoolSize
- 线程池中的核心线程数,默认情况下,核心线程一直存活在线程池中,即便他们在线程池中处于闲置状态。
- 除非我们将ThreadPoolExecutor的allowCoreThreadTimeOut属性设为true的时候,这时候处于闲置的核心线程在等待新任务到来时会有超时策略,这个超时时间由keepAliveTime来指定。一旦超过所设置的超时时间,闲置的核心线程就会被终止。
- 2.maximumPoolSize
- 线程池中所容纳的最大线程数,如果活动的线程达到这个数值以后,后续的新任务将会被阻塞。包含核心线程数+非核心线程数。
- 3.keepAliveTime
- 非核心线程闲置时的超时时长,对于非核心线程,闲置时间超过这个时间,非核心线程就会被回收。
- 只有对ThreadPoolExecutor的allowCoreThreadTimeOut属性设为true的时候,这个超时时间才会对核心线程产生效果。
- 4.unit
- 用于指定keepAliveTime参数的时间单位。他是一个枚举,可以使用的单位有天(TimeUnit.DAYS),小时(TimeUnit.HOURS),分钟(TimeUnit.MINUTES),毫秒(TimeUnit.MILLISECONDS)等等;
- 5.workQueue
- 线程池中保存等待执行的任务的阻塞队列。通过线程池中的execute方法提交的Runnable对象都会存储在该队列中。
- 我们可以选择下面几个阻塞队列。我们还能够通过实现BlockingQueue接口来自定义我们所需要的阻塞队列。 | 阻塞队列 | 说明 | | ------- | -------- | | ArrayBlockingQueue | 基于数组实现的有界的阻塞队列,该队列按照FIFO(先进先出)原则对队列中的元素进行排序。| | LinkedBlockingQueue | 基于链表实现的阻塞队列,该队列按照FIFO(先进先出)原则对队列中的元素进行排序。| | SynchronousQueue | 内部没有任何容量的阻塞队列。在它内部没有任何的缓存空间。对于SynchronousQueue中的数据元素只有当我们试着取走的时候才可能存在。| | PriorityBlockingQueue | 具有优先级的无限阻塞队列。|
- 6.threadFactory
- 线程工厂,为线程池提供新线程的创建。ThreadFactory是一个接口,里面只有一个newThread方法。 默认为DefaultThreadFactory类。
- 7.handler
- 是RejectedExecutionHandler对象,而RejectedExecutionHandler是一个接口,里面只有一个rejectedExecution方法。当任务队列已满并且线程池中的活动线程已经达到所限定的最大值或者是无法成功执行任务,这时候ThreadPoolExecutor会调用RejectedExecutionHandler中的rejectedExecution方法。在ThreadPoolExecutor中有四个内部类实现了RejectedExecutionHandler接口。在线程池中它默认是AbortPolicy,在无法处理新任务时抛出RejectedExecutionException异常。
- 下面是在ThreadPoolExecutor中提供的四个可选值。
- 我们也可以通过实现RejectedExecutionHandler接口来自定义我们自己的handler。如记录日志或持久化不能处理的任务。
可选值 说明 CallerRunsPolicy 只用调用者所在线程来运行任务。 AbortPolicy 直接抛出RejectedExecutionException异常。 DiscardPolicy 丢弃掉该任务,不进行处理。 DiscardOldestPolicy 丢弃队列里最近的一个任务,并执行当前任务。
- 如下图所示
image
3.2 ThreadPoolExecutor使用
- 如下所示,
ExecutorService service = new ThreadPoolExecutor(5, 10, 10, TimeUnit.SECONDS, new LinkedBlockingQueue<>());
- 对于ThreadPoolExecutor有多个构造方法,对于上面的构造方法中的其他参数都采用默认值。
- 可以通过execute和submit两种方式来向线程池提交一个任务。
- execute
- 当我们使用execute来提交任务时,由于execute方法没有返回值,所以说我们也就无法判定任务是否被线程池执行成功。
service.execute(new Runnable() { public void run() { System.out.println("execute方式"); } });
- submit
- 当我们使用submit来提交任务时,它会返回一个future,我们就可以通过这个future来判断任务是否执行成功,还可以通过future的get方法来获取返回值。
- 如果子线程任务没有完成,get方法会阻塞住直到任务完成,而使用get(long timeout, TimeUnit unit)方法则会阻塞一段时间后立即返回,这时候有可能任务并没有执行完。
Future<Integer> future = service.submit(new Callable<Integer>() { @Override public Integer call() throws Exception { System.out.println("submit方式"); return 2; } }); try { Integer number = future.get(); } catch (ExecutionException e) { e.printStackTrace(); }
- 线程池关闭
- 调用线程池的
shutdown()
或shutdownNow()
方法来关闭线程池 - shutdown原理:将线程池状态设置成SHUTDOWN状态,然后中断所有没有正在执行任务的线程。
- shutdownNow原理:将线程池的状态设置成STOP状态,然后中断所有任务(包括正在执行的)的线程,并返回等待执行任务的列表。
- 中断采用interrupt方法,所以无法响应中断的任务可能永远无法终止。 但调用上述的两个关闭之一,isShutdown()方法返回值为true,当所有任务都已关闭,表示线程池关闭完成,则isTerminated()方法返回值为true。当需要立刻中断所有的线程,不一定需要执行完任务,可直接调用shutdownNow()方法。
- 调用线程池的
3.3 四种线程池类用途
- FixedThreadPool
- 通过Executors的newFixedThreadPool()方法创建,它是个线程数量固定的线程池,该线程池的线程全部为核心线程,它们没有超时机制且排队任务队列无限制,因为全都是核心线程,所以响应较快,且不用担心线程会被回收。
- 重用指定数目(nThreads)的线程,其背后使用的是无界的工作队列,任何时候最多有 nThreads 个工作线程是活动的。这意味着,如果任务数量超过了活动队列数目,将在工作队列中等待空闲线程出现;如果有工作线程退出,将会有新的工作线程被创建,以补足指定的数目 nThreads。
- CachedThreadPool
- 通过Executors的newCachedThreadPool()方法来创建,它是一个数量无限多的线程池,它所有的线程都是非核心线程,当有新任务来时如果没有空闲的线程则直接创建新的线程不会去排队而直接执行,并且超时时间都是60s,所以此线程池适合执行大量耗时小的任务。由于设置了超时时间为60s,所以当线程空闲一定时间时就会被系统回收,所以理论上该线程池不会有占用系统资源的无用线程。
- 具有几个鲜明特点:它会试图缓存线程并重用,当无缓存线程可用时,就会创建新的工作线程;如果线程闲置的时间超过 60 秒,则被终止并移出缓存;长时间闲置时,这种线程池,不会消耗什么资源。其内部使用 SynchronousQueue 作为工作队列。
- ScheduledThreadPool
- 通过Executors的newScheduledThreadPool()方法来创建,ScheduledThreadPool线程池像是上两种的合体,它有数量固定的核心线程,且有数量无限多的非核心线程,但是它的非核心线程超时时间是0s,所以非核心线程一旦空闲立马就会被回收。这类线程池适合用于执行定时任务和固定周期的重复任务。
- SingleThreadExecutor
- 通过Executors的newSingleThreadExecutor()方法来创建,它内部只有一个核心线程,它确保所有任务进来都要排队按顺序执行。它的意义在于,统一所有的外界任务到同一线程中,让调用者可以忽略线程同步问题。
- newWorkStealingPool(int parallelism)
- 这是一个经常被人忽略的线程池,Java 8 才加入这个创建方法,其内部会构建ForkJoinPool,利用Work-Stealing算法,并行地处理任务,不保证处理顺序。
04.四种线程池类说明
- Java中四种具有不同功能常见的线程池。
- 他们都是直接或者间接配置ThreadPoolExecutor来实现他们各自的功能。这四个线程池可以通过Executors类获取。
- 这四种线程池分别是newFixedThreadPool;newCachedThreadPool;newScheduledThreadPool;newSingleThreadExecutor。
4.1 newFixedThreadPool
- 通过Executors中的newFixedThreadPool方法来创建,该线程池是一种线程数量固定的线程池。
ExecutorService service = Executors.newFixedThreadPool(4);
- 在这个线程池中 所容纳最大的线程数就是我们设置的核心线程数。
- 如果线程池的线程处于空闲状态的话,它们并不会被回收,除非是这个线程池被关闭。如果所有的线程都处于活动状态的话,新任务就会处于等待状态,直到有线程空闲出来。
- 由于newFixedThreadPool只有核心线程,并且这些线程都不会被回收,也就是它能够更快速的响应外界请求 。
- 从下面的newFixedThreadPool方法的实现可以看出,newFixedThreadPool只有核心线程,并且不存在超时机制,采用LinkedBlockingQueue,所以对于任务队列的大小也是没有限制的。
public static ExecutorService newFixedThreadPool(int nThreads) { return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>()); }
- 来看一下使用案例和输入的日志信息
- 创建了一个线程数为5的固定线程数量的线程池,同理该线程池支持的线程最大并发数也是5,模拟20个任务让它处理,执行任务。最后我们获取线程的信息,打印日志。
private void newFixedThreadPool() { ExecutorService fixedThreadPool = Executors.newFixedThreadPool(5); for (int i = 1; i <= 20; i++) { final int index = i; fixedThreadPool.execute(new Runnable() { @Override public void run() { String threadName = Thread.currentThread().getName(); Log.e("潇湘剑雨", "线程:"+threadName+",正在执行第" + index + "个任务"); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } } }); } }
4.2 newCachedThreadPool
- 通过Executors中的newCachedThreadPool方法来创建。
public static ExecutorService newCachedThreadPool() { return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>()); }
- 通过s上面的newCachedThreadPool方法在这里我们可以看出它的 核心线程数为0, 线程池的最大线程数Integer.MAX_VALUE。而Integer.MAX_VALUE是一个很大的数,也差不多可以说 这个线程池中的最大线程数可以任意大。
- 当线程池中的线程都处于活动状态的时候,线程池就会创建一个新的线程来处理任务。该线程池中的线程超时时长为60秒,所以当线程处于闲置状态超过60秒的时候便会被回收。
- 这也就意味着若是整个线程池的线程都处于闲置状态超过60秒以后,在newCachedThreadPool线程池中是不存在任何线程的,所以这时候它几乎不占用任何的系统资源。
- 对于newCachedThreadPool他的任务队列采用的是SynchronousQueue,上面说到在SynchronousQueue内部没有任何容量的阻塞队列。SynchronousQueue内部相当于一个空集合,我们无法将一个任务插入到SynchronousQueue中。所以说在线程池中如果现有线程无法接收任务,将会创建新的线程来执行任务。
- 来看一下使用案例和输入的日志信息
- 为了体现该线程池可以自动根据实现情况进行线程的重用,而不是一味的创建新的线程去处理任务,我设置了每隔1s去提交一个新任务,这个新任务执行的时间也是动态变化的。
private void newCachedThreadPool() { ExecutorService cachedThreadPool = Executors.newCachedThreadPool(); for (int i = 1; i <= number; i++) { final int index = i; try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } cachedThreadPool.execute(new Runnable() { @Override public void run() { String threadName = Thread.currentThread().getName(); Log.v("潇湘剑雨newCachedThreadPool", "线程:" + threadName + ",正在执行第" + index + "个任务"); try { long time = index * 500; Thread.sleep(time); } catch (InterruptedException e) { e.printStackTrace(); } } }); } }
4.3 newScheduledThreadPool
- 通过Executors中的newScheduledThreadPool方法来创建。
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) { return new ScheduledThreadPoolExecutor(corePoolSize); } public ScheduledThreadPoolExecutor(int corePoolSize) { super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS, new DelayedWorkQueue()); }
- 它的核心线程数是固定的,对于非核心线程几乎可以说是没有限制的,并且当非核心线程处于限制状态的时候就会立即被回收。
- 创建一个可定时执行或周期执行任务的线程池:
ScheduledExecutorService service = Executors.newScheduledThreadPool(4); service.schedule(new Runnable() { public void run() { System.out.println(Thread.currentThread().getName()+"延迟三秒执行"); } }, 3, TimeUnit.SECONDS); service.scheduleAtFixedRate(new Runnable() { public void run() { System.out.println(Thread.currentThread().getName()+"延迟三秒后每隔2秒执行"); } }, 3, 2, TimeUnit.SECONDS);
- 输出结果:
pool-1-thread-2延迟三秒后每隔2秒执行
pool-1-thread-1延迟三秒执行
pool-1-thread-1延迟三秒后每隔2秒执行
pool-1-thread-2延迟三秒后每隔2秒执行
pool-1-thread-2延迟三秒后每隔2秒执行
- 部分方法说明
schedule(Runnable command, long delay, TimeUnit unit)
:延迟一定时间后执行Runnable任务;schedule(Callable callable, long delay, TimeUnit unit)
:延迟一定时间后执行Callable任务;scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit)
:延迟一定时间后,以间隔period时间的频率周期性地执行任务;scheduleWithFixedDelay(Runnable command, long initialDelay, long delay,TimeUnit unit)
:与scheduleAtFixedRate()方法很类似,但是不同的是scheduleWithFixedDelay()方法的周期时间间隔是以上一个任务执行结束到下一个任务开始执行的间隔,而scheduleAtFixedRate()方法的周期时间间隔是以上一个任务开始执行到下一个任务开始执行的间隔,也就是这一些任务系列的触发时间都是可预知的。- ScheduledExecutorService功能强大,对于定时执行的任务,建议多采用该方法。
- 来看一下使用案例和输入的日志信息
- 通过日志可以发现schedule方法的任务只是执行了一次,然后每隔2秒执行一次该scheduleAtFixedRate方法中的任务
private void newScheduledThreadPool() { ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(5); //延迟2秒后执行该任务 scheduledThreadPool.schedule(new Runnable() { @SuppressLint("LongLogTag") @Override public void run() { String threadName = Thread.currentThread().getName(); Log.e("潇湘剑雨newScheduledThreadPool", "线程:" + threadName + ",正在执行"); } }, 2, TimeUnit.SECONDS); //延迟1秒后,每隔2秒执行一次该任务 scheduledThreadPool.scheduleAtFixedRate(new Runnable() { @Override public void run() { String threadName = Thread.currentThread().getName(); Log.e("潇湘剑雨", "线程:" + threadName + ",正在执行"); } }, 1, 2, TimeUnit.SECONDS); }
4.4 newSingleThreadExecutor
- 通过Executors中的newSingleThreadExecutor方法来创建
- 在这个线程池中只有一个核心线程,对于任务队列没有大小限制,也就意味着这一个任务处于活动状态时,其他任务都会在任务队列中排队等候依次执行。
- newSingleThreadExecutor将所有的外界任务统一到一个线程中支持,所以在这个任务执行之间我们不需要处理线程同步的问题。
public static ExecutorService newSingleThreadExecutor() { return new FinalizableDelegatedExecutorService (new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>())); }
- 来看一下使用案例和输入的日志信息
- 改了线程池的实现方式,即依次一个一个的处理任务,而且都是复用一个线程,日志为
private void newSingleThreadExecutor() { ExecutorService singleThreadPool = Executors.newSingleThreadExecutor(); for (int i = 1; i <= number; i++) { final int index = i; singleThreadPool.execute(new Runnable() { @Override public void run() { String threadName = Thread.currentThread().getName(); Log.v("潇湘剑雨", "线程:"+threadName+",正在执行第" + index + "个任务"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } }); } }
4.5 newWorkStealingPool
4.6 线程池如何选择
4.7 如何给线程赋予名字
- 实际开发遇到使用线程池的问题
- 使用线程池,默认情况下创建的线程名字都类似pool-1-thread-2这样,没有业务含义。而很多情况下为了便于诊断问题,都需要给线程赋予一个有意义的名字。
- 如何给线程池中的线程起一个名字
- 第一种:guava的ThreadFactoryBuilder.setNameFormat可以指定一个前缀,使用%d表示序号;
- 第二种:实现ThreadFactory并制定给线程池,在实现的ThreadFactory中设定计数和调用Thread.setName
05.线程池常用方法原理
5.0 线程池整体流程图
5.1 线程池构造流程
5.2 线程池初始化和容量
5.3 线程池execute
5.4 获取线程池状态
5.5 线程池任务队列原理
5.6 线程池拒绝策略原理
06.线程池实践的总结
6.1 执行流程介绍
- 大概的流程图如下
image
- 文字描述如下
- ①如果在线程池中的线程数量没有达到核心的线程数量,这时候就回启动一个核心线程来执行任务。
- ②如果线程池中的线程数量已经超过核心线程数,这时候任务就会被插入到任务队列中排队等待执行。
- ③由于任务队列已满,无法将任务插入到任务队列中。这个时候如果线程池中的线程数量没有达到线程池所设定的最大值,那么这时候就会立即启动一个非核心线程来执行任务。
- ④如果线程池中的数量达到了所规定的最大值,那么就会拒绝执行此任务,这时候就会调用RejectedExecutionHandler中的rejectedExecution方法来通知调用者。
6.2 线程池的使用技巧
- 需要针对具体情况而具体处理,不同的任务类别应采用不同规模的线程池,任务类别可划分为CPU密集型任务、IO密集型任务和混合型任务。(N代表CPU个数)
任务类别 说明 CPU密集型任务 线程池中线程个数应尽量少,如配置N+1个线程的线程池。 IO密集型任务 由于IO操作速度远低于CPU速度,那么在运行这类任务时,CPU绝大多数时间处于空闲状态,那么线程池可以配置尽量多些的线程,以提高CPU利用率,如2*N。 混合型任务 可以拆分为CPU密集型任务和IO密集型任务,当这两类任务执行时间相差无几时,通过拆分再执行的吞吐率高于串行执行的吞吐率,但若这两类任务执行时间有数据级的差距,那么没有拆分的意义。
6.3 线程池实践问题总结
- 线程池虽然为提供了非常强大、方便的功能,但是也不是银弹,使用不当同样会导致问题。
- 避免任务堆积。
- 前面我说过 newFixedThreadPool 是创建指定数目的线程,但是其工作队列是无界的,如果工作线程数目太少,导致处理跟不上入队的速度,这就很有可能占用大量系统内存,甚至是出现 OOM。诊断时,你可以使用 jmap 之类的工具,查看是否有大量的任务对象入队。
- 避免过度扩展线程。
- 通常在处理大量短时任务时,使用缓存的线程池,比如在最新的 HTTP/2 client API 中,目前的默认实现就是如此。我们在创建线程池的时候,并不能准确预计任务压力有多大、数据特征是什么样子(大部分请求是 1K 、100K 还是 1M 以上?),所以很难明确设定一个线程数目。
- 另外,如果线程数目不断增长(可以使用 jstack 等工具检查),也需要警惕另外一种可能性,就是线程泄漏,这种情况往往是因为任务逻辑有问题,导致工作线程迟迟不能被释放。建议你排查下线程栈,很有可能多个线程都是卡在近似的代码处。
- 避免死锁等同步问题
- 对于死锁的场景和排查。
- 尽量避免在使用线程池时操作ThreadLocal
- 通过今天的线程池学习,应该更能理解其原因,工作线程的生命周期通常都会超过任务的生命周期。
6.4 线程池大小选择策略
- 线程池大小不合适,太多或太少,都会导致麻烦
- 所以我们需要去考虑一个合适的线程池大小。虽然不能完全确定,但是有一些相对普适的规则和思路。
- 如果我们的任务主要是进行计算,那么就意味着 CPU 的处理能力是稀缺的资源,我们能够通过大量增加线程数提高计算能力吗?往往是不能的,如果线程太多,反倒可能导致大量的上下文切换开销。所以,这种情况下,通常建议按照 CPU 核的数目 N 或者 N+1。
- 如果是需要较多等待的任务,例如 I/O 操作比较多,可以参考 Brain Goetz 推荐的计算方法:
线程数 = CPU核数 × 目标CPU利用率 ×(1 + 平均等待时间/平均工作时间)
- 这些时间并不能精准预计,需要根据采样或者概要分析等方式进行计算,然后在实际中验证和调整。
- 另外,在实际工作中,不要把解决问题的思路全部指望到调整线程池上,很多时候架构上的改变更能解决问题,比如利用背压机制的Reactive Stream、合理的拆分等。
6.5 项目中用到多个线程池
- 遇到问题描述
- 一个项目中如果多个业务需要用到线程池,是定义一个公共的线程池比较好,还是按照业务定义各自不同的线程池?
- 如果不同的业务各自定义不同的线程池,那线程数的理论值也是按照前面的去计算吗?
- 建议
- 建议不同类别的业务用不同的线程池,至于线程池的数量,各自计算各自的,然后去做压测。
- 虽然你的系统有多个线程池,但是并不是所有的线程池里的线程都是忙碌的,你只需要针对有性能瓶颈的业务优化就可以了。
1.4 线程池的优势
①降低系统资源消耗
- 通过重用已存在的线程,降低线程创建和销毁造成的消耗;
②提高系统响应速度
- 当有任务到达时,无需等待新线程的创建便能立即执行;
③方便线程并发数的管控
- 线程若是无限制的创建,不仅会额外消耗大量系统资源,更是占用过多资源而阻塞系统或oom等状况,从而降低系统的稳定性。线程池能有效管控线程,统一分配、调优,提供资源使用率;
④更强大的功能
- 线程池提供了定时、定期以及可控线程数等功能的线程池,使用方便简单。
速度优化:线程池优化
- https://juejin.cn/post/7368690450898173952