JUC多线程:一篇文章搞懂线程池:从核心参数到源码设计全解析

文章提示

💡 本文涵盖线程池的核心概念、参数配置实践、常见问题排查、源码级原理剖析,以及Tomcat与JUC线程池设计对比。附带代码案例和通俗类比,新手也能轻松理解!


目录

文章提示

 文章前言

一、线程池的基本概念

什么是线程池?

线程池的架构:

 二、Executors的4种快捷创建线程池的方法

 1.newSingleThreadExecutor创建“单线程化线程池”

  2.newFixedThreadPool创建“固定数量的线程池”

  3.newCachedThreadPool创建“可缓存线程池”

 4.newScheduledThreadPool创建“可调度线程池”

三、参数配置与Coding实践

1.底层原理:使用ThreadPoolExecutor是Java中最常用的线程池实现类

 1. 设计模式:生产者-消费者

2. 核心线程 vs 非核心线程

3. 任务获取与执行流程

4. 线程回收与关闭

2. 线程池核心参数解析

3.线程执行过程

四、拒绝策略

五、Tomcat线程池 vs JUC线程池

一、区别:

二、Tomcat线程池核心参数

三、Tomcat线程池工作原理

六、总结与代码实战

文章总结


文章前言

在多线程编程中,线程池是提升性能、避免资源浪费的利器。但若使用不当,轻则程序卡顿,重则引发内存溢出。本文从实际开发出发,结合源码解析和常见面试题,用通俗易懂的语言拆解线程池的设计哲学、参数配置技巧及避坑指南。无论你是刚接触线程池的新手,还是想深入源码的进阶者,这里都有你需要的答案!


一、线程池的基本概念

什么是线程池?

线程池是一种“线程复用”机制,通过预先创建一定数量的线程,管理它们的生命周期,避免频繁创建和销毁线程的开销。
举个栗子:线程池就像一个“餐厅后厨”,核心线程是固定厨师,非核心线程是临时工,任务队列是顾客排队区。

优势:控制运行的线程数量,处理过程中将任务放入队列,然后在线程创建后启动这些任务,如果线程数量超过最大数量,超出了的线程排队等候,等待其他线程执行 再从队列中取出任务来执行

特点:降低资源消耗 提高响应速度 提高线程的可等性 Java中的线程池是通过executor框架实现的

线程池的架构:

  在这里就简单的介绍一下这几个类和接口吧:

1.Executor:他就是最顶层的接口,下面的类和接口都是直接间接的继承和实现与他的,其内部就只包含一个接收Runnable类型的异步任务的execute方法。
2.ExecutorService:他继承于Executor,在其内部提供了“接收异步任务并转交给执行者”的方法,如submit系列方法、 invoke系列方法等,具体如下:
        //向线程池提交单个异步任务
 <T > Future < T > submit(Callable < T > task);
        //向线程池提交批量异步任务
 <T > List < Future < T >> invokeAll(Collection < ? extends
        Callable<T>>tasks)throws InterruptedException;
3.AbstractExecutorService:它是一个抽象类,实现了ExecutorService 接口。其存在的目的就是为ExecutorService中的接口提供默认实现。
4.ThreadPoolExecutor:它就是大名鼎鼎的“线程池”实现类,它继承AbstractExecutorService抽象类。 基本上线程池的运用就是围绕他来展开的。

5.ScheduledExecutorService:是一个接口,它继承于 ExecutorService。它是一个可以完成“延时”和“周期性”任务的调度线 程池接口。

6.ScheduledThreadPoolExecutor:它继承于ThreadPoolExecutor,它提供了ScheduledExecutorService线程池接口中“延时执行”和“周期执行” 等抽象调度方法的具体实现。四大快捷创建线程池中newScheduledThreadPool就与他有关。

7.Executors:它是一个静态工厂类,通过其静态工厂方法返回 ExecutorService、ScheduledExecutorService等线程池示例对象,这些静态工厂方法可以理解为一些快捷的创建线程池的方法。但是在实际项目中已经明令禁止使用他去创建线程池了,下文中会给与解释。


 二、Executors的4种快捷创建线程池的方法

  虽然已经禁止使用他去创建线程池,但是并不妨碍去学习他。Java通过Executors工厂类提供了4种快捷创建线程池的方法,如下图:

 1.newSingleThreadExecutor创建“单线程化线程池”

        调用Executors.newSingleThreadExecutor()方法可用于创建一个“单线程化线程池”,然后使用FinalizableDelegatedExecutorService对该“固定大小的线程池”进行包装,这一层包装的作用是防止线程池的corePoolSize被动态地修改。也就是只有一个线程的线程池,所创建的线程池用唯一的工作线程来执行任务,使用此方法创建的线程池能保证所有任务按照指定顺序(如FIFO)执行。其池中的唯一线程的存活时间是无限的。当池中的唯一线程正繁忙时,新提交的任务实例会进入内部的阻塞队列LinkedBlockingQueue中,并且其阻塞队列是无界的。

        使用Executors创建的“单线程化线程池”潜在问题存在于其workQueue属性上,该属性的值为 LinkedBlockingQueue(无界阻塞队列)。如果任务提交速度持续大于任务处理速度,就会造成队列大量阻塞。如果队列很大,很有可能导致JVM的OOM异常,甚至造成内存资源耗尽。

        总体来说,单线程化的线程池所适用的场景是:任务按照提交次序,一个任务一个任务地逐个执行的场景。

    public static ExecutorService newSingleThreadExecutor() {
        return new FinalizableDelegatedExecutorService(new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue()));
    }
 
    public static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory) {
        return new FinalizableDelegatedExecutorService(new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue(), threadFactory));
    }

  2.newFixedThreadPool创建“固定数量的线程池”

        调用Executors.newFixedThreadPool(int threads)方法可用于创建一个“固定数量的线程池”,其唯一的参数用于设置池中线程的“固定数量”。其特点如下:

在使用时如果线程数没有达到“固定数量”,每次提交一个任务线程池内就创建一个新线程,直到线程达到线程池固定的数量。
线程池的大小一旦达到“固定数量”就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。
在接收异步任务的执行目标实例时,如果池中的所有线程均在繁忙状态,新任务会进入到无界的阻塞队列中LinkedBlockingQueue。
        使用Executors创建“固定数量的线程池”的潜在问题主要存在于其workQueue上,其值为LinkedBlockingQueue(无界阻塞队列)。 如果任务提交速度持续大于任务处理速度,就会造成队列中大量的任务等待。如果队列很大,很有可能导致JVM出现OOM(Out Of Memory)异常,即内存资源耗尽。

        总体来说,他的的适用场景:需要任务长期执行的场景。 “固定数量的线程池”的线程数能够比较稳定地保证一个数,能够避免频繁回收线程和创建线程,故适用于处理CPU密集型的任务,在CPU 被工作线程长时间占用的情况下,能确保尽可能少地分配线程。

    public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue());
    }
    public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) {
        return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue(), threadFactory);
    }

  3.newCachedThreadPool创建“可缓存线程池”

调用Executors.newCachedThreadPool()方法可用于创建一个“可缓存线程池”,如果线程池内的某些线程无事可干成为空闲线程,“可缓存线程池”可灵活回收这些空闲线程。他的特点如下:

在接收新的任务时,如果池内所有线程繁忙,此线程池就会添加新线程来处理任务。
此线程池不会对线程池大小进行限制,线程池大小完全依赖于操作系统(或者说JVM)能够创建的最大线程大小。
如果部分线程空闲,也就是存量线程的数量超过了处理任务数量,就会在60秒后回收空闲线程。
        使用Executors创建的“可缓存线程池”的潜在问题存在于其最大线程数量不设限上。由于其maximumPoolSize的值为 Integer.MAX_VALUE(非常大),可以认为可以无限创建线程,如果任务提交较多,就会造成大量的线程被启动,很有可能造成OOM异常,甚至导致CPU线程资源耗尽。

       它的适用场景是需要快速处理突发性强、耗时较短 的任务场景。如Netty的NIO处理场景。该线程池的线程数量不固定,只要有空闲线程就会被回收;接收到的新异步任务执行目标,查看是否有线程处于空闲状态,如果没有就直接创建新的线程。

public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue());
    }
 
    public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue(), threadFactory);
    }

 4.newScheduledThreadPool创建“可调度线程池”

        调用Executors.newScheduledThreadPool(int corePoolSize)方法可用于创建一个“可调度线程池”,即一个提供“延时”和“周期性”任务调度功能的ScheduledExecutorService类型的线程池。

    public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
        return new ScheduledThreadPoolExecutor(corePoolSize);
    }
 
    public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize, ThreadFactory threadFactory) {
        return new ScheduledThreadPoolExecutor(corePoolSize, threadFactory);
    }
    public ScheduledThreadPoolExecutor(int corePoolSize) {
        super(corePoolSize, Integer.MAX_VALUE, 10L, TimeUnit.MILLISECONDS, new DelayedWorkQueue());
    }

        它既是ScheduledExecutorService又是ThreadPool的实例,在ScheduleExecutorService接口 中有多个重要的接收被调目标任务的方法,其中scheduleAtFixedRate 和scheduleWithFixedDelay使用得比较多。

        使用Executors创建的“可缓存线程池”的潜在问题存在于其最大线程数量不设限上。由于其线程数量不设限,如果到期任务太多, 就会导致CPU的线程资源耗尽。

        该线程池的适用场景是周期性地执行任务的场景。其中Spring Boot中的任务调度器的底层就是借助了JUC的ScheduleExecutorService“可调度线程池”实现的。

package JUC.pool;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
// Executors 工具类、3大方法
public class Demo01 {
    public static void main(String[] args) {
        //ExecutorService threadPool = Executors.newSingleThreadExecutor();// 单个线程
         //ExecutorService threadPool = Executors.newFixedThreadPool(5); // 创建个固定的线程池的大小
        ExecutorService threadPool = Executors.newCachedThreadPool(); // 可伸缩的,遇强则强,遇弱则弱
        try {
            for (int i = 0; i < 100; i++) {
        // 使用了线程池之后,使用线程池来创建线程
                threadPool.execute(()->{
                    System.out.println(Thread.currentThread().getName()+" ok");
                });
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
        // 线程池用完,程序结束,关闭线程池
            threadPool.shutdown();
        }
    }
}

三、参数配置与Coding实践

底层原理:使用ThreadPoolExecutor是Java中最常用的线程池实现类

 1. 设计模式:生产者-消费者
  • 生产者:调用submit()提交任务。

  • 消费者:Worker线程从队列中拉取任务执行。

2. 核心线程 vs 非核心线程
  • 本质无区别!区分仅在于:
    核心线程默认常驻,非核心线程空闲超时后被回收。

  • 源码真相

// ThreadPoolExecutor.addWorker()  
if (workerCount >= corePoolSize) break; // 核心线程数满后不再新增核心线程
3. 任务获取与执行流程
  1. 任务提交后,优先由核心线程执行。

  2. 核心线程满时,任务进入队列排队。

  3. 队列满时,创建非核心线程处理任务。

  4. 非核心线程也满时,触发拒绝策略。

4. 线程回收与关闭
  • 回收:非核心线程通过getTask()中的超时机制自动销毁。

  • 关闭

executor.shutdown(); // 优雅关闭,等待队列任务完成
executor.shutdownNow(); // 暴力关闭,尝试中断所有线程

2. 线程池核心参数解析

ThreadPoolExecutor executor = new ThreadPoolExecutor(
    corePoolSize,   // 核心线程数(常驻厨师)
    maximumPoolSize, // 最大线程数(总厨师上限)
    keepAliveTime,  // 非核心线程空闲存活时间(临时工摸鱼时间)
    TimeUnit.MILLISECONDS, // 时间单位
    new LinkedBlockingQueue<>(100), // 任务队列(排队区容量)
    new ThreadPoolExecutor.AbortPolicy() // 拒绝策略(排队区满后的处理)
);

参数配置原则:

  • CPU密集型:核心线程数 ≈ CPU核数(避免过多线程竞争CPU)。

  • IO密集型:核心线程数可适当增大(如2*CPU核数,线程等待IO时不占用CPU)。

1. 需要分析线程池执行的任务的特性: CPU 密集型还是 IO 密集型
2. 每个任务执行的平均时长大概是多少,这个任务的执行时长可能还跟任务处理逻 辑是否涉及到网络传输以及底层系统资源依赖有关系。 如果是 CPU 密集型,主要是执行计算任务,响应时间很快,cpu 一直在运行, 这种任务 cpu 的利用率很高,那么线程数的配置应该根据 CPU 核心数来决定, CPU 核心数=最大同时执行线程数,加入 CPU 核心数为 4,那么服务器最多能 同时执行 4 个线程。过多的线程会导致上下文切换反而使得效率降低。那线程池的最大线程数可以配置为 cpu 核心数+1 如果是 IO 密集型,主要是进行 IO 操 作,执行 IO 操作的时间较长,这是 cpu 出于空闲状态,导致 cpu 的利用率不 高,这种情况下可以增加线程池的大小。这种情况下可以结合线程的等待时长来做判断,等待时间越高,那么线程数也相对越多。一般可以配置 cpu 核心数的 2 倍。
一个公式:
线程池设定最佳线程数目 =
         ((线程池设定的线程等待时间+线程 CPU 时间) / 线程 CPU 时间 ) * CPU 数目
这个公式的  '线程 cpu 时间'  是预估的程序单个线程在 cpu 上运行的时间(通常使用 loadrunner 测试大量运行次数求出平均值)

3.线程执行过程

图示:

1.提交任务后会首先进行当前工作线程数与核心线程数的比较,如果当前工作线程数小于核心线程数,则直接调用 addWorker() 方法创建一个核心线程去执行任务;

2.如果工作线程数大于核心线程数,即线程池核心线程数已满,则新任务会被添加到阻塞队列中等待执行,当然,添加队列之前也会进行队列是否为空的判断;

3.如果线程池里面存活的线程数已经等于核心线程数了,且阻塞队列已经满了,再会去判断当前线程数是否已经达到最大线程数 maximumPoolSize,如果没有达到,则会调用 addWorker() 方法创建一个非核心线程去执行任务;

4.如果当前线程的数量已经达到了最大线程数时,当有新的任务提交过来时,会执行拒绝策略

总结来说就是优先核心线程、阻塞队列次之,最后非核心线程。


四、拒绝策略

  • AbortPolicy(默认):直接抛出RejectedExecutionException异常。

ThreadPoolExecutor executor = new ThreadPoolExecutor(
    2, 4, 60, TimeUnit.SECONDS, new ArrayBlockingQueue<>(5),
    new ThreadPoolExecutor.AbortPolicy());
  • CallerRunsPolicy:不在新线程中执行任务,而是由调用线程执行该任务。

ThreadPoolExecutor executor = new ThreadPoolExecutor(
    2, 4, 60, TimeUnit.SECONDS, new ArrayBlockingQueue<>(5),
    new ThreadPoolExecutor.CallerRunsPolicy());
  • DiscardPolicy:不做任何处理,直接丢弃任务。

ThreadPoolExecutor executor = new ThreadPoolExecutor(
    2, 4, 60, TimeUnit.SECONDS, new ArrayBlockingQueue<>(5),
    new ThreadPoolExecutor.DiscardPolicy());
  • DiscardOldestPolicy:丢弃队列中最老的任务,然后尝试重新提交当前任务。

ThreadPoolExecutor executor = new ThreadPoolExecutor(
    2, 4, 60, TimeUnit.SECONDS, new ArrayBlockingQueue<>(5),
    new ThreadPoolExecutor.DiscardOldestPolicy());

五、Tomcat线程池 vs JUC线程池

一、区别:

1. 任务队列处理策略不同

  • JUC线程池:任务提交时,优先入队,队列满后才创建非核心线程。

    • 1. 核心线程执行任务 → 2. 队列满 → 3. 创建非核心线程 → 4. 触发拒绝策略

  • Tomcat线程池队列只是缓冲,优先创建线程到maxThreads,队列满后直接触发拒绝策略。

    • 1. 核心线程执行 → 2. 创建线程到maxThreads → 3. 队列满 → 4. 拒绝请求

2. 设计目标不同

  • JUC线程池:通用任务处理(如计算密集型任务)。

  • Tomcat线程池高并发HTTP请求处理,需快速响应,避免请求堆积。

二、Tomcat线程池核心参数

server.xml或Spring Boot的application.properties中配置:

# Spring Boot配置示例
server.tomcat.threads.max=200       # 最大线程数(默认200)
server.tomcat.threads.min-spare=10  # 核心线程数(默认10)
server.tomcat.accept-count=100      # 等待队列容量(默认100)
server.tomcat.max-connections=10000 # 最大连接数(默认取决于系统)

参数详解

  • maxThreads:同时处理请求的最大线程数(类比餐厅最大厨师数量)。

  • accept-count:等待队列长度(排队区座位数),队列满后直接拒绝请求。

  • maxConnections:TCP连接池大小(餐厅总座位数,包含已用餐和排队的)。

三、Tomcat线程池工作原理

1. 请求处理流程

2. 关键组件

  • Acceptor线程:接收TCP连接,放入连接池。

  • Poller线程:监听Socket事件(如可读),将就绪的请求交给Worker线程池。

  • Executor(Worker线程池):实际处理HTTP请求的线程池

特性JUC线程池Tomcat线程池
队列满时的处理先创建非核心线程先创建线程到maxPoolSize
任务队列设计通用阻塞队列优先使用无界队列
适用场景通用计算任务高并发Web请求处理

六、总结与代码实战

总结要点:

  1. 参数配置是门艺术:根据任务类型(CPU/IO)动态调整参数。

  2. 异常处理不能忘:用Futuretry-catch捕获任务异常。

  3. 关闭线程池要优雅:避免强制关闭导致数据丢失。

代码实战:自定义线程池包含拒绝策略

package JUC.pool;

import java.util.concurrent.*;
/**
 * new ThreadPoolExecutor.AbortPolicy() // 银行满了,还有人进来,不处理这个人的,抛出异常
 * new ThreadPoolExecutor.CallerRunsPolicy() // 哪来的去哪里!
 * new ThreadPoolExecutor.DiscardPolicy() //队列满了,丢掉任务,不会抛出异常!
 * new ThreadPoolExecutor.DiscardOldestPolicy() //队列满了,尝试去和最早的竞争,也不会
 抛出异常!
 */
public class Demo02 {
    public static void main(String[] args) {
        // 自定义线程池!工作 ThreadPoolExecutor
        // 1.corePoolSize:核心池的大小
        // 2.maximumPoolSize:线程池最大线程数
        // 3.keepAliveTime:线程没有任务时最多保持多久时间会终止
        // 4.unit:keepAliveTime参数的时间单位
        // 5.workQueue:阻塞队列,当核心线程都被占用,且阻塞队列已满时,线程池会创建新的线程,
        // 6.threadFactory:线程工厂,用于创建线程
        // 7.handler:拒绝策略,当阻塞队列已满,且线程池线程数达到最大时,用于拒绝请求的
        //为什么要自定义线程池? 在阿里手册上,有写,线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor,因为
        //Executors线程池创建出来的线程,可能会存在线程内存溢出的风险。
        // 线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式,这样的处理方式让写的同学更加明确线程池的运行规则,
        // 避免API使用过程中,线程池使用方式不理解导致一些问题。Executors使用方法如下:
        ExecutorService threadPool = new ThreadPoolExecutor(
                2,
                5,
                3,
                TimeUnit.SECONDS,
                new LinkedBlockingDeque<>(3),
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.DiscardOldestPolicy()); //队列满了,尝试去和最早的竞争,也不会抛出异常!
                //new ThreadPoolExecutor.AbortPolicy() // 银行满了,还有人进来,不处理这个人的,抛出异常
                //new ThreadPoolExecutor.CallerRunsPolicy() // 哪来的去哪里!
                //new ThreadPoolExecutor.DiscardPolicy() //队列满了,丢掉任务,不会抛出异常!
        try {
            // 最大承载:Deque + max
            // 超过 RejectedExecutionException
            for (int i = 1; i <= 9; i++) {
                // 使用了线程池之后,使用线程池来创建线程
                threadPool.execute(()->{
                    System.out.println(Thread.currentThread().getName()+" ok");
                });
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            // 线程池用完,程序结束,关闭线程池
            threadPool.shutdown();
        }
    }
}

 

 


文章总结

线程池就像个“智能调度中心”,核心是平衡资源与性能。假设你是快递站长:

  1. 核心快递员(核心线程):每天固定上班,处理日常包裹。

  2. 临时工(非核心线程):双十一爆仓时临时雇佣,忙完就下班。

  3. 快递柜(任务队列):包裹先放柜子,快递员按顺序取。

  4. 爆柜处理(拒绝策略):柜子满了?让客户自己来取(CallerRunsPolicy)!

新手常见误区

  • 以为核心线程数越大越好?错!就像雇100个快递员在淡季摸鱼,浪费钱!

  • 不处理任务异常?就像快递丢了不通知客户,迟早被投诉!

记住:用好线程池,程序稳如狗! 🚀

参考:

线程池及其底层工作原理_线程池的工作原理-CSDN博客

 并发编程——线程池篇_juc的几种线程池-CSDN博客

Java ThreadPoolExecutor详解:任务提交、调度与线程管理-CSDN博客

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值