深入浅出java线程池执行过程

引言

在Java并发编程中,线程池是一种被广泛应用的资源管理工具,它能够有效地管理和控制程序中的线程数量,从而提高系统性能、减少系统资源消耗。本文将深入剖析Java线程池的工作机制与执行过程。

一、线程池的基本概念

Java中的线程池主要由java.util.concurrent ThreadPoolExecutor类实现,其核心组件包括:工作队列(如ArrayBlockingQueue)、线程池(包含核心线程和非核心线程)以及拒绝策略等。线程池的主要任务是接收外部提交的任务并调度线程进行执行。

二、线程池的工作流程

直接上图:

线程池执行过程主要分为四个阶段

任务提交阶段

当我们调用ThreadPoolExecutor.execute(Runnable task)方法向线程池提交一个任务时,线程池内部会经历以下步骤:

  1. 判断当前线程池中的线程数量是否小于核心线程数:如果是,线程池会创建一个新的工作线程来执行这个任务,并将其初始化,然后调用线程的start()方法启动它。
  2. 若当前线程数不小于核心线程数,线程池会尝试将任务添加到工作队列(如ArrayBlockingQueue)。这里的"尝试"意味着如果队列已满,那么就会触发下一种处理方式。
  3. 如果工作队列已满,线程池会检查当前线程数是否小于最大线程数。如果小于最大线程数,线程池会创建一个新的线程来处理这个任务;否则,就需要采取拒绝策略来处理无法接受的新任务。

任务获取与执行阶段

每个工作线程都在一个无限循环中运行,它们会不断从工作队列中获取任务并执行。获取任务的过程通常涉及阻塞等待(如使用BlockingQueue的take()方法)。一旦获取到任务,线程就会执行Runnable.run()方法,完成任务的实际工作。

线程扩展与收缩

  • 线程扩展 :当线程池需要创建新线程来处理任务时,会调用ThreadFactory.newThread(Runnable r)方法创建新线程。默认的线程工厂会为每个线程赋予有意义的名字,比如"pool-1-thread-1"。
  • 线程收缩:线程池中的非核心线程在完成任务后不会立即销毁,而是进入保持存活状态。只有当这些线程在指定时间内(keepAliveTime参数指定)没有接收到新的任务,并且线程池中的线程数大于核心线程数时,这些空闲线程才会被终止。

拒绝策略

四种常见的拒绝策略如下:

  • AbortPolicy:默认拒绝策略,直接抛出RejectedExecutionException异常。
  • CallerRunsPolicy:调用者所在线程负责执行任务,这将降低新任务的提交速度。
  • DiscardPolicy:默默地丢弃任务,不执行也不抛出异常。
  • DiscardOldestPolicy:移除工作队列中最旧的任务(最先入队但尚未被执行的任务),然后重新尝试提交当前任务。

线程池核心参数

Java线程池的核心类是java.util.concurrent.ThreadPoolExecutor,它提供了丰富的参数用于定制线程池的行为。以下是ThreadPoolExecutor的主要构造函数参数及其含义:

  1. corePoolSize: 这是线程池的基本大小,即线程池即使在空闲时也会维持的最小线程数量。只要有任务提交过来,就会优先创建至corePoolSize个线程来处理任务。
  2. maximumPoolSize: 这是线程池能容纳的最大线程数。当线程池中的所有线程都处于活动状态且工作队列已满时,线程池会尝试增加更多的线程来处理任务,直到达到这个上限。
  3. keepAliveTime: 当线程池中线程数量超过corePoolSize时,多余的空闲线程在多长时间内(无新任务提交)仍不被使用就会被终止。这个时间长度是以TimeUnit指定的时间单位来衡量的。
  4. TimeUnit: keepAliveTime参数的时间单位,可以是纳秒(NANOSECONDS)、微秒(MICROSECONDS)、毫秒(MILLISECONDS)、秒(SECONDS)、分钟(MINUTES)、小时(HOURS)或天(DAYS)。
  5. BlockingQueue workQueue: 任务队列,用于存储等待执行的任务。可以选择不同的队列类型,例如无界队列(如LinkedBlockingQueue)、有界队列(如ArrayBlockingQueue)或其他满足BlockingQueue接口的自定义队列。
  6. ThreadFactory threadFactory: 用于创建新线程的工厂类,可以通过自定义ThreadFactory来设定新线程的名称、优先级和其他属性。
  7. RejectedExecutionHandler handler: 拒绝策略,当线程池和任务队列都无法处理新的任务时,会调用这个策略的rejectedExecution方法来处理被拒绝的任务。Java提供了一系列内置的拒绝策略如AbortPolicy、CallerRunsPolicy、DiscardPolicy和DiscardOldestPolicy,也可以自定义拒绝策略。

线程池实践

创建一个核心线程池数为2,最大线程数为3,阻塞队列大小为2的线程池。

下面的代码中,执行一次任务需要5000MS,而6个任务是一次性提交进去的,其中第四个任务就会因为 无法被核心线程执行,无法加入等待队列,无法创建新的非核心线程执行,而执行拒绝策略。

java 复制代码
public static void main(String[] args) throws InterruptedException {
        ThreadPoolExecutor executorService = new ThreadPoolExecutor(2,
                3,
                0, TimeUnit.MILLISECONDS,
                new LinkedBlockingQueue<Runnable>(2));
​
        for (int i = 0; i < 6; i++) {
            executorService.execute(() -> {
                try {
                    Date date = new Date();
                    System.out.println("线程:" + Thread.currentThread().getName() + "报时:" + date);
                    Thread.sleep(5000);
                } catch (InterruptedException e) {
                }
​
            });
​
            System.out.println("等待队列中现在有" + executorService.getQueue().size() + "个任务");
            Thread.sleep(500);
        }
    }

运行结果如下:

从运行结果中,可以看出

1、前面两个任务是通过核心线程池来执行

2、第3、4个任务会被放到等待队列中

3、等待队列满了后,第五个任务是通过创建了一个非核心线程来执行任务,这是第三个线程

4、第六个任务由于核心线程池,等待队列,最大线程池数都已经达到最大,所以执行拒绝策略

5、从队列中获取任务,由于核心线程里面的任务已经执行完成,所以通过核心线程执行

相关推荐
光电的一只菜鸡几秒前
高通tuning中eis需要调什么
java·开发语言·前端
API快乐传递者1 小时前
1688 跨境电商 API 接口实战指南:从寻源到代采的全链路技术方案
java·前端·数据库
孔明click331 小时前
不想写代码,但想要集成一个登录页面?Sa-Token-Quick-Login 帮你实现!
java·sa-token·开源·springboot·登录·权限·权限认证
曹牧1 小时前
C#与Java后台交互
java·windows·microsoft·c#
张晓祥-长草颜团子1 小时前
【重庆两江新区歌词】
java
东小西1 小时前
【SAA实战】第 2 篇:模型与消息——ReactAgent 怎么挑模型、怎么传消息
java·后端·spring
码匠许师傅1 小时前
【C++ 面试真题】35. 聊聊 C++ 的万能引用(T&&)和完美转发(std::forward)
java·c++·面试
十年Java程序媛1 小时前
注解已经加上,但是事务没有回滚?拆解 AOP 代理底层原因 + 代码示例
java
μθημα1 小时前
K8s Pod 管理实战:虚拟机环境下的具体命令与步骤
java·docker·kubernetes
数据狐(Datafox)2 小时前
1688.item_search工程实战:1688商品列表API分页采集与B2B货源数据分析落地
java·前端·数据分析