Java线程池,白话文vs八股文,原来是这么回事!

简介: 本文介绍了Java线程池的原理、实现方式及相关参数。首先,通过类比公司员工的方式解释了线程池的核心概念,如核心线程、最大线程数、任务队列和拒绝策略。接着,详细描述了线程池的任务处理流程,并提供了使用`ThreadPoolExecutor`和`Executors`创建线程池的代码示例,强调了`ThreadPoolExecutor`的灵活性和`Executors`的局限性。最后,总结了线程池的相关参数及不同类型的线程池实现,并附带常见面试题及其解答,帮助读者全面理解线程池的应用场景和优化方法。

一、线程池原理


添加图片注释,不超过 140 字(可选)


1、白话文篇


添加图片注释,不超过 140 字(可选)



添加图片注释,不超过 140 字(可选)


1.1、正式员工(corePoolSize)

正式员工:这些是公司最稳定和最可靠的长期员工,他们一直在工作,不会被解雇或者辞职。他们负责处理公司的核心业务,比如生产、销售、财务等。在Java线程池中,正式员工对应于核心线程(corePoolSize),这些线程会一直存在于线程池中。他们负责执行线程池中的任务,如果没有任务,他们会等待新的任务到来。

1.2、所有员工(maximumPoolSize)

所有员工:这些是公司所有的员工,包括正式员工和外包员工。他们共同组成了公司的团队,协作完成公司的各种业务。在Java线程池中,所有员工对应于所有线程(maximumPoolSize),这些线程是线程池能够创建的最大数量的线程。他们都可以执行线程池中的任务,如果没有任务,他们会等待新的任务到来。


1.3、外包员工(maximumPoolSize - corePoolSize)

外包员工:这些是公司根据业务需求临时雇佣的员工,他们只在有额外的任务时才会被雇佣,如果没有任务,他们会被解雇或者辞职。他们也可以负责处理公司的业务,比如活动、项目、临时需求等。在Java线程池中,外包员工对应于非核心线程(maximumPoolSize - corePoolSize),这些线程只在核心线程不足以处理所有任务时才会被创建,他们也负责执行线程池中的任务,如果没有任务,他们会等待新的任务到来。


1.4、排队任务(workQueue)

排队任务:这是公司用来存放待处理任务的地方,比如订单、合同、报告等。每个任务都有一个优先级和一个截止日期,根据这些信息来决定哪个任务先处理,哪个任务后处理。在Java线程池中,任务队列对应于BlockingQueue,这是一个用来存放Runnable对象的阻塞队列。每个Runnable对象都代表一个要执行的任务,根据队列的类型和容量来决定哪个任务先入队,哪个任务后入队。当有空闲的线程时,它会从队列中取出一个任务来执行。

1.5、拒绝策略(handler)

拒绝策略:这是公司用来处理无法接受或者无法完成的任务的方法也会选择相应的放弃和别的策略,比如退单、转单、延期等。每个策略都有一个风险和一个收益,根据公司的目标和资源来选择合适的策略。在Java线程池中,拒绝策略对应于RejectedExecutionHandler,这是一个用来处理无法执行的任务的接口。每个实现类都代表一个不同的策略,根据线程池的状态和参数来选择合适的策略。

2、八股文篇

一个线程提交到线程池的处理流程如下图


添加图片注释,不超过 140 字(可选)


1)初始化线程池,线程池初始化时并没有创建corePoolSize数目的核心线程,而是惰性加载的方式。等有任务后才创建核心线程。


添加图片注释,不超过 140 字(可选)


2)如果线程池中的数量小于corePoolSize,即使线程池中的线程都处于空闲状态,也要创建新的核心线程来处理被添加的任务。

3)如果线程池中的数量大于等于corePoolSize,但是缓冲队列workQueue未满,那么任务被放入缓冲队列。

4)如果线程池中的数量大于等于corePoolSize,缓冲队列workQueue满,并且线程池中的数量小于maximumPoolSize,建新的非核心线程来处理被添加的任务。

5)如果此时线程池中的数量大于等于corePoolSize,缓冲队列workQueue满,并且线程池中的数量等于maximumPoolSize,那么通过 handler所指定的策略来处理此任务。

二、线程池实现


添加图片注释,不超过 140 字(可选)


2.1、ThreadPoolExecutor线程池(推荐)

public class ThreadPoolExecutorDemo {     public static void main(String[] args) {         // 创建一个线程池对象         ThreadPoolExecutor executor = new ThreadPoolExecutor(             2, 5, 10, TimeUnit.SECONDS,             new ArrayBlockingQueue<>(5), Executors.defaultThreadFactory(),             new ThreadPoolExecutor.AbortPolicy());         // 提交多个任务到线程池中         for (int i = 1; i <= 10; i++) {             executor.execute(() -> {                 try {                     Thread.sleep(1000);                     System.out.println(Thread.currentThread().getName() + " is running");                 } catch (InterruptedException e) {                     e.printStackTrace();                 }             });         }         // 关闭线程池         executor.shutdown();     } }

该示例创建了一个核心线程数为 2,最大线程数为 5,等待队列大小为 5 的线程池对象,然后提交了 10 个任务到线程池中。每个任务会休眠 1 秒钟,然后输出当前线程的名称。最后,调用 shutdown() 方法关闭线程池。

添加图片注释,不超过 140 字(可选)


当线程池任务处理不过来的时候,可以通过handler指定的策略进行处理,ThreadPoolExecutor提供了四种策略:

1)ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常;也是默认的处理方式。

2)ThreadPoolExecutor.DiscardPolicy:丢弃任务,但是不抛出异常。

3)ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程)

4)ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务

可以通过实现RejectedExecutionHandler接口自定义处理方式。

2.2、Executors线程池(不推荐)

import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class ExecutorsDemo {     public static void main(String[] args) {         // 创建一个固定大小的线程池对象         ExecutorService executor = Executors.newFixedThreadPool(3);         // 提交多个任务到线程池中         for (int i = 1; i <= 10; i++) {             executor.execute(() -> {                 try {                     Thread.sleep(1000);                     System.out.println(Thread.currentThread().getName() + " is running");                 } catch (InterruptedException e) {                     e.printStackTrace();                 }             });         }         // 关闭线程池         executor.shutdown();     } }

该示例使用 Executors 工厂类创建了一个固定大小为 3 的线程池对象,然后提交了 10 个任务到线程池中。每个任务会休眠 1 秒钟,然后输出当前线程的名称。最后,调用 shutdown() 方法关闭线程池。

需要注意的是,虽然 Executors 提供了许多快速创建线程池对象的方法,但是这些方法并不能满足所有的需求和场景,因此在实际应用中,需要根据具体情况和性能需求选择合适的线程池实现,并进行适当的参数设置和优化等操作。以下是几种创建方式:

1)Executors.newCachedThreadPool(); 说明: 创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程. 内部实现:new ThreadPoolExecutor(0,Integer.MAX_VALUE,60L,TimeUnit.SECONDS,new SynchronousQueue());

2)Executors.newFixedThreadPool(int); 说明: 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。 内部实现:new ThreadPoolExecutor(nThreads, nThreads,0L,TimeUnit.MILLISECONDS,new LinkedBlockingQueue());

3)Executors.newSingleThreadExecutor(); 说明:创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照顺序执行。 内部实现:new ThreadPoolExecutor(1,1,0L,TimeUnit.MILLISECONDS,new LinkedBlockingQueue())

4)Executors.newScheduledThreadPool(int); 说明:创建一个定长线程池,支持定时及周期性任务执行。 内部实现:new ScheduledThreadPoolExecutor(corePoolSize)

【附】阿里巴巴Java开发手册中对线程池的使用规范


添加图片注释,不超过 140 字(可选)


2.3、相关参数说明

2.3.1、线程池的等待队列

1)ArrayBlockingQueue: 这是一个由数组实现的容量固定的有界阻塞队列.

2)SynchronousQueue: 没有容量,不能缓存数据;每个put必须等待一个take; offer()的时候如果没有另一个线程在poll()或者take()的话返回false。

3)LinkedBlockingQueue: 这是一个由单链表实现的默认无界的阻塞队列。LinkedBlockingQueue提供了一个可选有界的构造函数,而在未指明容量时,容量默认为Integer.MAX_VALUE。

队列操作:


添加图片注释,不超过 140 字(可选)


2.3.2、线程数值

Oracle 官方并没有给出线程池 corePoolSize 的具体参考值,因为这个值的大小应该根据实际业务场景和系统资源情况来进行优化调整。不同的业务场景和系统资源状况可能需要不同的 corePoolSize 设置。

不过,在《Java并发编程实战》一书中给出了建议。 在这本书中,作者 Brian Goetz 等人指出,线程池的规模应该根据任务类型和计算密集度来确定。

添加图片注释,不超过 140 字(可选)


  • 对于 CPU 密集型任务,应该将核心线程数设置为处理器核心数加 1 或者 2;
  • 对于 I/O 密集型任务,可以适当增加核心线程数以利用空闲的 CPU 时间。具体大小可以根据实际情况进行调整,建议设置在 2 x N 左右,其中 N 是可用 CPU 核心数。

这个建议是基于以下考虑:对于 CPU 密集型任务,线程需要大量计算,因此需要足够多的 CPU 资源,而处理器核心数加 1 或者 2 的数量可以充分利用 CPU 资源,避免线程之间的竞争和阻塞;而对于 I/O 密集型任务,由于线程大部分时间都处于等待 I/O 操作的状态,因此可以适当增加核心线程数以利用空闲的 CPU 时间,从而提高系统效率。虽然这个建议并非官方标准,但在实际应用中已经得到广泛的认可和应用,并取得了不错的效果。

三、相关题目

  1. 什么是Java线程池?它的作用是什么?

答:Java线程池是一种用于管理和重用线程的机制。它的主要作用是减少线程创建和销毁的开销,提高线程的重用性,以优化多线程应用的性能。

  1. Java线程池的核心参数是什么?请解释它们的含义。

答:Java线程池的核心参数包括:

  • 核心线程池大小(Core Pool Size):线程池中保持活动的最小线程数量。
  • 最大线程池大小(Maximum Pool Size):线程池允许的最大线程数量。
  • 任务队列(work Queue):用于存储等待执行的任务的数据结构。
  • 线程存活时间(Thread Keep-Alive Time):在没有任务时,超过核心线程数的线程保持存活的时间。
  1. Java线程池中的任务执行顺序是什么样的?

答:任务执行顺序可以根据任务提交方式和线程池类型来变化。通常,线程池会按照任务提交的顺序执行任务,但线程池类型不同,如FixedThreadPool和SingleThreadPool,可能会有不同的执行策略。对于有优先级的任务,线程池可以根据任务优先级来决定执行顺序。

  1. 什么是线程池的拒绝策略(Rejection Policy)?

答:拒绝策略定义了当任务队列已满且线程池无法创建更多线程时,如何处理新提交的任务。Java线程池提供了几种内置的拒绝策略,如AbortPolicy(默认,拒绝并抛出异常)、CallerRunsPolicy(调用者线程执行任务)、DiscardPolicy(默默丢弃任务)和DiscardOldestPolicy(丢弃队列中最老的任务)。

  1. Java中有哪些线程池实现类,它们有什么不同?

答:Java中有多种线程池实现类,包括:

  • FixedThreadPool:固定大小的线程池,核心线程数和最大线程数相等,无任务队列。
  • CachedThreadPool:根据需要创建新线程的线程池,适用于处理大量短生命周期的任务。
  • SingleThreadPool:只包含一个线程的线程池,用于按顺序执行任务。
  • ScheduledThreadPool:用于定时或周期性执行任务的线程池。
  • 自定义线程池:可以通过ThreadPoolExecutor类进行自定义线程池配置。



目录
相关文章
|
3月前
|
存储 缓存 安全
HashMap VS TreeMap:谁才是Java Map界的王者?
HashMap VS TreeMap:谁才是Java Map界的王者?
140 2
|
3月前
|
数据采集 缓存 Java
Python vs Java:爬虫任务中的效率比较
Python vs Java:爬虫任务中的效率比较
|
20天前
|
安全 Java 程序员
ArrayList vs Vector:一场线程安全与性能优化的世纪之争!
在 Java 面试中,ArrayList 和 Vector 是高频考点,但很多人容易混淆。本文通过10分钟深入解析它们的区别,帮助你快速掌握性能、线程安全性、扩容机制等核心知识,让你轻松应对面试题目,提升自信!
49 18
|
3月前
|
安全 Java 程序员
Java集合之战:ArrayList vs LinkedList,谁才是你的最佳选择?
本文介绍了 Java 中常用的两个集合类 ArrayList 和 LinkedList,分析了它们的底层实现、特点及适用场景。ArrayList 基于数组,适合频繁查询;LinkedList 基于链表,适合频繁增删。文章还讨论了如何实现线程安全,推荐使用 CopyOnWriteArrayList 来提升性能。希望帮助读者选择合适的数据结构,写出更高效的代码。
120 3
|
5月前
|
Java C++ 开发者
if-else VS switch:谁才是Java条件判断的王者?
if-else VS switch:谁才是Java条件判断的王者?
54 3
|
5月前
|
传感器 C# 监控
硬件交互新体验:WPF与传感器的完美结合——从初始化串行端口到读取温度数据,一步步教你打造实时监控的智能应用
【8月更文挑战第31天】本文通过详细教程,指导Windows Presentation Foundation (WPF) 开发者如何读取并处理温度传感器数据,增强应用程序的功能性和用户体验。首先,通过`.NET Framework`的`Serial Port`类实现与传感器的串行通信;接着,创建WPF界面显示实时数据;最后,提供示例代码说明如何初始化串行端口及读取数据。无论哪种传感器,只要支持串行通信,均可采用类似方法集成到WPF应用中。适合希望掌握硬件交互技术的WPF开发者参考。
93 0
|
5月前
|
C# Windows 开发者
当WPF遇见OpenGL:一场关于如何在Windows Presentation Foundation中融入高性能跨平台图形处理技术的精彩碰撞——详解集成步骤与实战代码示例
【8月更文挑战第31天】本文详细介绍了如何在Windows Presentation Foundation (WPF) 中集成OpenGL,以实现高性能的跨平台图形处理。通过具体示例代码,展示了使用SharpGL库在WPF应用中创建并渲染OpenGL图形的过程,包括开发环境搭建、OpenGL渲染窗口创建及控件集成等关键步骤,帮助开发者更好地理解和应用OpenGL技术。
390 0
|
5月前
|
开发者 C# 容器
【独家揭秘】当WPF邂逅DirectX:看这两个技术如何联手打造令人惊艳的高性能图形渲染体验,从环境搭建到代码实践,一步步教你成为图形编程高手
【8月更文挑战第31天】本文通过代码示例详细介绍了如何在WPF应用中集成DirectX以实现高性能图形渲染。首先创建WPF项目并使用SharpDX作为桥梁,然后在XAML中定义承载DirectX内容的容器。接着,通过C#代码初始化DirectX环境,设置渲染逻辑,并在WPF窗口中绘制图形。此方法适用于从简单2D到复杂3D场景的各种图形处理需求,为WPF开发者提供了高性能图形渲染的技术支持和实践指导。
336 0
|
5月前
|
Java 数据库连接 微服务
揭秘微服务架构下的数据魔方:Hibernate如何玩转分布式持久化,实现秒级响应的秘密武器?
【8月更文挑战第31天】微服务架构通过将系统拆分成独立服务,提升了可维护性和扩展性,但也带来了数据一致性和事务管理等挑战。Hibernate 作为强大的 ORM 工具,在微服务中发挥关键作用,通过二级缓存和分布式事务支持,简化了对象关系映射,并提供了有效的持久化策略。其二级缓存机制减少数据库访问,提升性能;支持 JTA 保证跨服务事务一致性;乐观锁机制解决并发数据冲突。合理配置 Hibernate 可助力构建高效稳定的分布式系统。
82 0
|
5月前
|
程序员 调度 C++
解锁Ruby并发编程新境界!Fiber与线程:轻量级VS重量级,你选哪一派引领未来?
【8月更文挑战第31天】Ruby提供了多种并发编程方案,其中Fiber与线程是关键机制。Fiber是自1.9版起引入的轻量级并发模型,无需独立堆栈和上下文切换,由程序员控制调度。线程则为操作系统级别,具备独立堆栈和上下文,能利用多核处理器并行执行。通过示例代码展示了Fiber和线程的应用场景,如任务调度和多URL数据下载,帮助开发者根据需求选择合适的并发模型,提升程序性能与响应速度。
68 0
下一篇
开通oss服务