一、线程池原理
1、白话文篇
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、八股文篇
一个线程提交到线程池的处理流程如下图
1)初始化线程池,线程池初始化时并没有创建corePoolSize数目的核心线程,而是惰性加载的方式。等有任务后才创建核心线程。
2)如果线程池中的数量小于corePoolSize,即使线程池中的线程都处于空闲状态,也要创建新的核心线程来处理被添加的任务。
3)如果线程池中的数量大于等于corePoolSize,但是缓冲队列workQueue未满,那么任务被放入缓冲队列。
4)如果线程池中的数量大于等于corePoolSize,缓冲队列workQueue满,并且线程池中的数量小于maximumPoolSize,建新的非核心线程来处理被添加的任务。
5)如果此时线程池中的数量大于等于corePoolSize,缓冲队列workQueue满,并且线程池中的数量等于maximumPoolSize,那么通过 handler所指定的策略来处理此任务。
二、线程池实现
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() 方法关闭线程池。
当线程池任务处理不过来的时候,可以通过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开发手册中对线程池的使用规范
2.3、相关参数说明
2.3.1、线程池的等待队列
1)ArrayBlockingQueue: 这是一个由数组实现的容量固定的有界阻塞队列.
2)SynchronousQueue: 没有容量,不能缓存数据;每个put必须等待一个take; offer()的时候如果没有另一个线程在poll()或者take()的话返回false。
3)LinkedBlockingQueue: 这是一个由单链表实现的默认无界的阻塞队列。LinkedBlockingQueue提供了一个可选有界的构造函数,而在未指明容量时,容量默认为Integer.MAX_VALUE。
队列操作:
2.3.2、线程数值
Oracle 官方并没有给出线程池 corePoolSize 的具体参考值,因为这个值的大小应该根据实际业务场景和系统资源情况来进行优化调整。不同的业务场景和系统资源状况可能需要不同的 corePoolSize 设置。
不过,在《Java并发编程实战》一书中给出了建议。 在这本书中,作者 Brian Goetz 等人指出,线程池的规模应该根据任务类型和计算密集度来确定。
- 对于 CPU 密集型任务,应该将核心线程数设置为处理器核心数加 1 或者 2;
- 对于 I/O 密集型任务,可以适当增加核心线程数以利用空闲的 CPU 时间。具体大小可以根据实际情况进行调整,建议设置在 2 x N 左右,其中 N 是可用 CPU 核心数。
这个建议是基于以下考虑:对于 CPU 密集型任务,线程需要大量计算,因此需要足够多的 CPU 资源,而处理器核心数加 1 或者 2 的数量可以充分利用 CPU 资源,避免线程之间的竞争和阻塞;而对于 I/O 密集型任务,由于线程大部分时间都处于等待 I/O 操作的状态,因此可以适当增加核心线程数以利用空闲的 CPU 时间,从而提高系统效率。虽然这个建议并非官方标准,但在实际应用中已经得到广泛的认可和应用,并取得了不错的效果。
三、相关题目
- 什么是Java线程池?它的作用是什么?
答:Java线程池是一种用于管理和重用线程的机制。它的主要作用是减少线程创建和销毁的开销,提高线程的重用性,以优化多线程应用的性能。
- Java线程池的核心参数是什么?请解释它们的含义。
答:Java线程池的核心参数包括:
- 核心线程池大小(Core Pool Size):线程池中保持活动的最小线程数量。
- 最大线程池大小(Maximum Pool Size):线程池允许的最大线程数量。
- 任务队列(work Queue):用于存储等待执行的任务的数据结构。
- 线程存活时间(Thread Keep-Alive Time):在没有任务时,超过核心线程数的线程保持存活的时间。
- Java线程池中的任务执行顺序是什么样的?
答:任务执行顺序可以根据任务提交方式和线程池类型来变化。通常,线程池会按照任务提交的顺序执行任务,但线程池类型不同,如FixedThreadPool和SingleThreadPool,可能会有不同的执行策略。对于有优先级的任务,线程池可以根据任务优先级来决定执行顺序。
- 什么是线程池的拒绝策略(Rejection Policy)?
答:拒绝策略定义了当任务队列已满且线程池无法创建更多线程时,如何处理新提交的任务。Java线程池提供了几种内置的拒绝策略,如AbortPolicy(默认,拒绝并抛出异常)、CallerRunsPolicy(调用者线程执行任务)、DiscardPolicy(默默丢弃任务)和DiscardOldestPolicy(丢弃队列中最老的任务)。
- Java中有哪些线程池实现类,它们有什么不同?
答:Java中有多种线程池实现类,包括:
- FixedThreadPool:固定大小的线程池,核心线程数和最大线程数相等,无任务队列。 CachedThreadPool:根据需要创建新线程的线程池,适用于处理大量短生命周期的任务。 SingleThreadPool:只包含一个线程的线程池,用于按顺序执行任务。 ScheduledThreadPool:用于定时或周期性执行任务的线程池。 自定义线程池:可以通过ThreadPoolExecutor类进行自定义线程池配置。