线程池及其运行原理

本文详细介绍了Java线程池的工作原理、优缺点、线程池的状态转换、常用的线程池工具类以及如何自定义线程池。通过分析线程池的运行机制,讨论了如何合理配置线程池以提高系统性能。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

前言

  • 首先从结构说起
  • 然后线程池的参数
  • 最后在结合代码简单分析

new Thread 弊端

        第一:每次new Thread 新建对象,性能差
        第二:线程缺乏统一管理,可能无限制的新建线程,相互竞争,有可能占用过多系统资源导致死机或OOM
        第三:缺少更多的功能,如更多执行、定期执行、线程中断。


什么是线程池

Java中的线程池是运用场景最多的并发框架,几乎所有需要异步或并发执行任务的程序,都可以使用线程池。在开发过程中,合理地使用线程池能够带来3个好处。
           第一:降低资源消耗。重用存在的线程,减少对象创建、消亡的开销,性能佳。
           第二:提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
           第三:提高线程的可管理性。线程是稀缺资源,如果无限制地创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一分配、调优和监控。但是,要做到合理利用、


线程池实例的几种状态

           running状态:能接受新提交的任务,并且能处理阻塞队列中的任务。
           shutdown状态:当一个线程池实例处于关闭状态的时候,不能在接收新提交的任务,但是可以继续处理阻塞队列中已经保存的任务。在线程池处理running状态时,它调用shutdown方法,会使线程池进入到该状态。
           stop状态:不能接收新的任务,也不处理队列中的任务。它会中断正在处理中的线程,在线程池处于running或shutdown状态时,如果调用shutdownNow的时候会使线程池进入到该状态
           tidying状态:如果所用的任务都终止了,有效线程数为0,线程池会进入到该状态。之后调用terminated()方法会进入terminated状态。
           terminated状态


线程池的体系结构

根目录是Executor在JUC包下(java.util.concurrent),结构如下(这里说的是在JUC包下的子类):

|- Executor :负责线程的使用与调用的根接口
    |--** ExecutorService    子接口:线程池的主要接口
        |-- AbstractExecutorService 提供 ExecutorService 执行方法的默认实现
              |-- DelegatedExecutorService 
              |-- ForkJoinPool
              |-- ThreadPoolExecutor    线程池的实现类
        |-- ScheduledExecutorService    子接口:负责线程的调度
              |-- ScheduledThreadPoolExecutor:继承了 ThreadPoolExecutor,实现了ScheduledExecutorService

实际上最根本用的是ExecutorService,又因为ExecutorService是接口,接口不能创建对象,所以根本用的就是创建ThreadPoolExecutor的实例或ScheduledThreadPoolExecutor的实例。

但是基本上都是使用Executors工具类。最常见的有如下四个。但是这四个类都有可能OOM,一般情况下都需要根据自己需求,自定义线程池。

1、newFixedThreadPool()   :  创建固定大小的线程池,返回类型是ExecutorService。
2、newCachedThreadPool() :  缓存线程池,线程池的数量不固定,可以根据需求自动的更改数量。返回类型是ExecutorService。
3、newSingleThreadExecutor() :   创建单个线程池,线程池中只有一个线程。返回类型是ExecutorService。
4、newScheduledThreadPool()  :  创建固定大小的线程池,可以延迟或定时的执行任务。返回类型ScheduledExecutorService。

ThreadPoolExecutor常用的方法

  • execute():提交任务,交给线程池执行。
  • submit():提交任务,能够返回执行结果 execute + Future
  • shutdown():关闭线程池,等待任务都执行完
  • shutdownNow():关闭线程池,不等待任务执行完
  • getTaskCount():线程池已执行和未执行的任务总数
  • getCompletedTaskCount():已完成的任务数量
  • getPoolSize():线程池当前的线程数量
  • getActiveCount();当前线程池中正在执行任务的线程数量

线程池工具类的参数介绍

在介绍线程池原理的时候首先看一下这四个工具类源码。 

           其实这四个工具类都是实例化ThreadPoolExecutor这个类。进入ThreadPoolExecutor里查看这个构造函数。ThreadPoolExecutor构造函数有4种。这里介绍参数最多的。

public ThreadPoolExecutor(int corePoolSize,   //核心线程数
                          int maximumPoolSize,//最大线程数
                          long keepAliveTime,//表示线程没有任务执行时最多保持多久时间会终止。
                          TimeUnit unit,     //参数keepAliveTime的时间单位,有7种取值
                          BlockingQueue<Runnable> workQueue, //阻塞队列,存储等待执行的任务,很重要,会对线程池运行过程产生重大影响
                          ThreadFactory threadFactory,  //线程工厂,用来创建线程
                          RejectedExecutionHandler handler //当拒绝处理任务时的策略
)
拒绝策略有四种
    1、AbortPolicy 默认 处理程序遭到拒绝将抛出运行时 RejectedExecutionException 
    2、CallerRunsPolicy  用调用者所在的线程调用任务
    3、DiscardOldestPolicy 如果执行程序没有关闭,阻塞队列头部的任务将被删除,然后重试执行程序(如果再次失败,则重复此过程)
    4、DiscardPolicy  不能执行的任务将被删除,只不过不抛出异常。

    如果运行的线程数少于corePoolSize的时候,直接创建新线程处理任务。
    如果运行的线程数大于等于corePoolSize,小于maximumPoolSize的时候,只有当workQueue满的时候才创建新的线程去处理任务。
    如果设置的corePoolSize和maximumPoolSize相同的话,那么创建线程池大小是固定的。这时候如果有请求,workQueue还没满的时候,就把请求放入workQueue中,等待有空闲的线程从这里面提取。

为什么线程池要用阻塞队列而不用非阻塞队列?
           因为线程执行需要时间,当队列满的情况下,遇到新的任务添加不进去,会出现丢任务的情况。用阻塞队列的话,可以阻
塞添加,等线程执行完,有空余线程,会执行阻塞队列里的任务,这样新的任务可以添加进去。

接下来具体介绍运行原理,以及核心线程数与最大线程数的关系。


线程池原理剖析

提交一个任务到线程池中,线程池的处理流程有三步:

1、判断线程池里的核心线程是否都在执行任务,如果不是(核心线程空闲或者还有核心线程没有被创建)则创建一个新的工作线程来执行任务。如果核心线程都在执行任务,则进入下个流程。

2、线程池判断阻塞队列是否已满,如果阻塞队列没有满,则将新提交的任务存储在这个阻塞队列里。如果阻塞队列满了,则进入下个流程。

3、判断线程池里的线程是否都处于工作状态,如果没有,则创建一个新的工作线程来执行任务(也是放在线程池里)。如果已经满了,则交给饱和策略来处理这个任务。

 


自定义线程池

 如果上图不理解的话,可以结合代码在想着可能会更好理解。根据四个工具类可以得出想要自定义线程池就实例化ThreadPoolExecutor即可,就是根据需要实例化ThreadPoolExecutor,如下图,自定义一个核心线程数为1,最大线程数为2,阻塞队列为ArrayBlockingQueue。

public class Test {
	public static void main(String[] args) {
            // 核心线程数是1,最大线程数是2,阻塞队列是采用ArrayBlockingQueue(有边界的阻塞队列)
	    ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(1, 2, 60L, TimeUnit.SECONDS, new ArrayBlockingQueue<>(3));
            //当执行第一个任务时,直接从线程池中取出线程执行
	    threadPoolExecutor .execute(new TaskThread("任务1"));
            //当执行第二个的时候,因为大于核心线程数,所以放在阻塞队列中
            threadPoolExecutor .execute(new TaskThread("任务2"));
            //当执行第三个的时候,因为大于核心线程数,所以放在阻塞队列中
            threadPoolExecutor .execute(new TaskThread("任务3"));
            //当执行第四个的时候,因为大于核心线程数,所以放在阻塞队列中
            threadPoolExecutor .execute(new TaskThread("任务4"));
            //当执行第五个的时候,因为大于核心线程数,且阻塞队列已经被占满,这个时候最大线程数还有空闲线程,所以新建一个线程执行该任务
            threadPoolExecutor .execute(new TaskThread("任务5"));
            //当放入第六个线程的时候,因为2个最大线程数和3个阻塞队列全被占用,所以会报错
            threadPoolExecutor .execute(new TaskThread("任务6"));
            //关闭线程池
	    threadPoolExecutor .shutdown();
	}
}
class TaskThread implements Runnable {
	private String taskName;
	public TaskThred(String taskName) {
		this.taskName = taskName;
	}
	@Override
	public void run() {
		System.out.println(Thread.currentThread().getName()+taskName);
	}
}

 


线程池的合理配置

1、CPU密集型:花费了绝大多数时间在计算上

CPU密集的意思是该任务需要大量的运算,而没有阻塞,CPU一直全速运行。 
           CPU密集任务只有在真正的多核CPU上才可能得到加速(通过多线程),而在单核CPU上,无论你开几个模拟的多线程,该任务都不可能得到加速,因为CPU总的运算能力就那些。

CPU密集型任务,就需要尽量压榨CPU,参考值可以设为CPU+1

2、I/O密集型:  花费了大多是时间在等待I/O上。

IO密集型,即该任务需要大量的IO,即大量的阻塞。在单线程上运行IO密集型的任务会导致浪费大量的CPU运算能力浪费在等待。所以在IO密集型任务中使用多线程可以大大的加速程序运行,即时在单核CPU上,这种加速主要就是利用了被浪费掉的阻塞时间。

IO密集型任务,参考值可以设置为2*CPU。

### Java线程池的实现原理 Java中的线程池主要通过`ThreadPoolExecutor`类来实现,它是整个线程池机制的核心组件。以下是关于线程池实现原理的具体说明: #### 1. 核心概念与参数解析 `ThreadPoolExecutor`是一个灵活的线程池实现类,它的构造函数允许开发者定义多个重要参数以控制线程池的行为[^1]。这些参数包括但不限于: - `corePoolSize`: 核心线程数量,在任何情况下都会保持活跃的状态。 - `maximumPoolSize`: 线程池能够容纳的最大线程数。 - `keepAliveTime`: 当前活动线程超过核心线程数时,多余的空闲线程等待新任务的时间长度。 - `workQueue`: 存储待处理任务的阻塞队列。 #### 2. 工作流程分析 当向线程池提交一个新任务时,线程池按照以下逻辑顺序进行处理[^2]: 1. 判断当前核心线程是否全部被占用。如果有未使用的线程,则分配给新的任务;否则继续下一步。 2. 如果核心线程已满,尝试将任务放入工作队列中。如果队列尚未达到上限,任务会被暂时存放在队列里等待执行。 3. 若队列也已填满,再次评估是否有可用的工作线程(不超过最大线程数)。若有则启动额外的新线程去完成此任务。 4. 假设以上条件均不满足,即无法创建更多线程也无法排队等候,那么依据设定好的拒绝策略决定如何处置该任务。 #### 3. 静态 vs 动态线程池 除了基本功能外,还存在一种特殊形式——动态线程池。这种类型的线程池具备更强适应性和灵活性,因为它可以根据实际运行状况实时调节自身的属性设置,比如调整核心线程数目或者改变队列大小等,从而更好地匹配不同的业务场景需求[^3]。 #### 4. 实际应用案例展示 下面给出一段简单的代码演示了如何利用`ThreadPoolExecutor`构建并操作自己的线程池实例[^4]: ```java public class ThreadPoolDemo { public static void main(String[] args) throws InterruptedException { // 初始化线程池 ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor( 3, // corePoolSize 5, // maximumPoolSize 0L, // keepAliveTime TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>(), Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy() ); // 提交三个任务至线程池 for (int i = 0; i < 6; i++) { final int taskNumber = i; threadPoolExecutor.submit(() -> { try { Thread.sleep(100); System.out.println("Task " + taskNumber + " is running on " + Thread.currentThread().getName()); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } }); } // 安全关闭线程池 threadPoolExecutor.shutdown(); while (!threadPoolExecutor.awaitTermination(1, TimeUnit.SECONDS)) {} } } ``` 上述例子展示了如何自定义一个拥有固定最小线程数但可扩展到一定限度内的线程池,并且展示了任务提交以及最终安全终止的过程。 #### 5. C++对比视角下的补充说明 值得注意的是,虽然本讨论聚焦于Java平台上的线程池设计模式及其具体实践方法论,但在其他编程语言领域同样存在着类似的解决方案。例如C++标准库提供了std::async作为轻量级异步任务调度工具之一,而第三方开源项目Boost.Asio更是实现了高度复杂的事件驱动型并发框架。尽管它们之间语法风格和技术细节有所差异,但从宏观角度来看,其背后蕴含的思想却是相通的:合理规划计算资源分布、优化I/O密集型作业效率等等[^5]。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值