欢迎光临
我们一直在努力

12.深入理解Java线程池:从底层原理到面试通关秘籍

目录

一、上节课重点回顾(温故而知新)

二、本节课重点:线程池(ThreadPoolExecutor)

1. 为什么需要线程池?

2. 线程池的核心概念与类比

3. ThreadPoolExecutor 详解

① 构造方法参数(重点!经典面试题)

② 线程工厂(ThreadFactory)—— 工厂设计模式

③ 拒绝策略(RejectedExecutionHandler)—— 面试必问!

④ 任务队列(BlockingQueue)

4. 代码实践:ThreadPoolExecutor 的使用

5. 两种用法的选择

三、学习思考与面试准备

1. 面试常问点总结

2. 我的学习感悟

3. 实习冲刺建议


大家好,我是正在冲刺大三上大厂实习的准大三生。最近在复习多线程进阶内容,尤其是线程池这一块,真的是面试必问的重灾区,也是实际开发中提升效率的关键。这篇博客我会结合自己的学习笔记,把线程池的核心知识点、底层原理、代码实践以及面试常考点都梳理一遍,希望能帮到同样在冲实习的你!


一、上节课重点回顾(温故而知新)

在开始线程池之前,先快速回顾一下上节课的核心内容,这些也是理解线程池的基础:

  • 多线程代码案例:常见的线程创建、同步、通信等基础操作。

  • 单例模式:确保一个类只有一个实例,比如饿汉式、懒汉式。

  • 懒汉模式线程安全:

    • if和 new包起来(加锁)。

    • 双重 if判断。

    • volatile关键字的使用。

  • 阻塞队列:

    • 阻塞队列 BlockingQueue。

    • 生产者消费者模型:降低竞争、解耦合、削峰填谷(MQ的核心思想)。

  • 经典面试题:synchronized和 wait/notify的使用,二次确认机制。


  • 二、本节课重点:线程池(ThreadPoolExecutor)

    1. 为什么需要线程池?

    • 进程 vs 线程:早期编程通过多进程实现并发,但进程创建销毁开销大。后来引入多线程(轻量级进程),但如果短时间内大量创建销毁线程,开销依然很大。

    • 池化思想:为了解决频繁创建销毁线程的性能问题,引入了“池”的概念。把线程提前创建好,放到一个位置里,需要的时候直接取,比操作系统创建快得多。

    • 用户态 vs 内核态:

      • 代码执行在用户态(应用程序)。

      • 操作系统内核态(负责CPU权限、硬件管理等)。

      • t.start()创建线程的过程:先执行用户态逻辑,然后调用操作系统API(内核态),再回到用户态。这种切换有开销。

      • 线程池的优势:提前创建好线程,后续直接用,纯用户态操作,避免了频繁的内核态切换,效率更高。

    2. 线程池的核心概念与类比

    我用一个生活中的例子来理解线程池:

    • 场景:去复印店复印身份证。

    • 自助复印机:立即去复印,马上拿回来(可控,纯用户态)。

    • 柜台小哥:拿着身份证去后台复印,可能会做其他事,过一会才给你(不可控,通过操作系统API,涉及内核态切换)。

    线程池就是“自助复印机”的集合——提前准备好线程,需要时直接取用。

    3. ThreadPoolExecutor 详解

    Java中线程池的核心类是 ThreadPoolExecutor,它的构造方法比较复杂,我们逐个拆解:

    ① 构造方法参数(重点!经典面试题)

    ThreadPoolExecutor(int corePoolSize,
    int maximumPoolSize,
    long keepAliveTime,
    TimeUnit unit,
    BlockingQueue<Runnable> workQueue,
    ThreadFactory threadFactory,
    RejectedExecutionHandler handler)

    • corePoolSize(核心线程数):线程池创建时,默认就会创建的线程数。即使空闲,也不会被销毁(除非设置了 allowCoreThreadTimeOut)。

    • maximumPoolSize(最大线程数):线程池允许的最大线程数。当任务队列满了,会创建非核心线程,直到达到这个最大值。

    • keepAliveTime + unit(非核心线程存活时间):非核心线程在空闲时,允许存活的最大时间。超过这个时间,就会被回收。

    • workQueue(任务队列):用于存放待执行的任务。常用的是阻塞队列(如 LinkedBlockingQueue、ArrayBlockingQueue)。当队列中没有任务时,线程会阻塞等待。

    • threadFactory(线程工厂):用于创建线程的工厂类。默认是 Executors.defaultThreadFactory(),但我们可以自定义,用于统一设置线程的名字、优先级、是否为后台线程等。

    • handler(拒绝策略):当任务队列满了,且线程数达到最大值时,新提交的任务该如何处理?这就是拒绝策略的作用。

    ② 线程工厂(ThreadFactory)—— 工厂设计模式

    线程池中有很多线程,可能需要统一设置它们的属性(比如名字、优先级、后台线程等)。这时候就需要线程工厂。

    • 工厂模式:为了解决构造方法重载过多、依赖构造方法的问题,引入工厂模式。通过不同的工厂方法,创建不同配置的对象。

    • ThreadFactory 接口:定义了创建线程的方法 newThread(Runnable r)。

    • 默认实现:Executors.defaultThreadFactory(),返回一个新的 Thread对象。

    • 自定义线程工厂:可以实现 ThreadFactory接口,在 newThread方法中设置线程的属性。

    举个例子,用工厂模式解决构造方法重载问题:

    // 通过伪代码描述问题:表示平面上的一个点
    class Point {
    // 构造方法1:通过横坐标、纵坐标
    public Point(double x, double y) {
    // …
    }
    // 构造方法2:通过极坐标的方式
    public Point(double r, double a) {
    // …
    }
    }
    //报错!!!!!!!!!!!!!!!!!!!
    // 上面的两个构造方法无法形成重载(参数类型相同,顺序不同但Java不允许)

    // 解决方案:引入工厂模式
    class PointFactory {
    // 工厂方法:通过直角坐标创建点
    public static Point newPointByXY(double x, double y) {
    Point p = new Point(x, y);
    // p.setXXX(); // 可以在这里设置其他属性
    return p;
    }
    // 工厂方法:通过极坐标创建点
    public static Point newPointByRA(double r, double a) {
    Point p = new Point(r, a);
    // p.setXXX();
    return p;
    }
    }

    回到线程池,ThreadFactory就是这个作用——为了统一设置线程的属性。

    ③ 拒绝策略(RejectedExecutionHandler)—— 面试必问!

    当任务队列满了,且线程数达到最大值时,新提交的任务会被拒绝。JDK提供了4种默认的拒绝策略:

  • AbortPolicy(默认):直接抛出 RejectedExecutionException异常,程序崩溃。

  • CallerRunsPolicy:让调用 submit的线程(调用者)自己执行这个任务。比如,你在主线程提交任务,主线程就去执行它。

  • DiscardOldestPolicy:把任务队列中最老的任务(最早加入但还没执行的)丢弃,然后尝试提交新任务。

  • DiscardPolicy:直接丢弃新任务,不做任何处理。

  • 如何选择拒绝策略?

    • 日常开发中,通常认为新任务比较重要,所以上述策略都是在尽力让新任务能执行。

    • 如果是消息队列场景,可能会选择丢弃旧任务(DiscardOldestPolicy)或新任务(DiscardPolicy)。

    • 如果是核心业务,可能选择 CallerRunsPolicy或抛出异常(AbortPolicy)。

    ④ 任务队列(BlockingQueue)

    线程池的任务队列必须是阻塞队列,因为当队列中没有任务时,线程会阻塞等待。常用的阻塞队列:

    • LinkedBlockingQueue:无界队列(默认容量是 Integer.MAX_VALUE)。

    • ArrayBlockingQueue:有界队列,需要指定容量。

    • SynchronousQueue:不存储元素的队列,每个插入操作必须等待一个移除操作。

    4. 代码实践:ThreadPoolExecutor 的使用

    下面是两个常见的线程池使用示例:

    示例1:使用 Executors 工厂类(简单场景)

    package thread;

    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;

    public class Demo1 {
    public static void main(String[] args) {
    // 创建一个固定大小的线程池(核心线程数=最大线程数=10)
    ExecutorService executorService = Executors.newFixedThreadPool(10);
    // 提交100个任务
    for (int i = 0; i < 100; i++) {
    // 此处id不需要动,是“常量”
    final int id = i;
    executorService.submit(new Runnable() {
    @Override
    public void run() {
    String name = Thread.currentThread().getName();
    System.out.println("hello " + name + "," + id);
    }
    });
    }
    // 关闭线程池(实际开发中要注意关闭时机)
    executorService.shutdown();
    }
    }

    示例2:使用 ThreadPoolExecutor 原生构造(精细化定制)

    package thread;

    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    import java.util.concurrent.ThreadPoolExecutor;

    public class Demo1 {
    public static void main(String[] args) {
    // 原生构造:核心线程数=5,最大线程数=10,存活时间=5秒,队列容量=100,使用默认工厂和拒绝策略
    ExecutorService executorService = new ThreadPoolExecutor(
    5, 10, 5, java.util.concurrent.TimeUnit.SECONDS,
    new java.util.concurrent.LinkedBlockingQueue<>(100),
    java.util.concurrent.Executors.defaultThreadFactory(),
    new ThreadPoolExecutor.AbortPolicy()
    );
    // 提交100个任务
    for (int i = 0; i < 100; i++) {
    final int id = i;
    executorService.submit(new Runnable() {
    @Override
    public void run() {
    String name = Thread.currentThread().getName();
    System.out.println("hello " + name + "," + id);
    }
    });
    }
    // 关闭线程池
    executorService.shutdown();
    }
    }

    5. 两种用法的选择

    • Executors 工厂类:简单易用,适合简单场景。但需要注意,默认的线程池配置可能有隐患

    • ThreadPoolExecutor 原生构造:适合精细化定制的场景,可以明确控制核心参数(队列容量、拒绝策略等),避免资源耗尽。


    三、学习思考与面试准备

    1. 面试常问点总结

    • 线程池的核心参数有哪些?各自的作用是什么?

    • 四种拒绝策略的区别?如何选择?

    • 线程工厂的作用?为什么要使用线程工厂?

    • Executors工厂类和 ThreadPoolExecutor原生构造的区别?为什么阿里开发手册推荐用原生构造?

    • 线程池中的线程是如何复用的?(核心线程不会被销毁,非核心线程在空闲时会被回收)

    2. 我的学习感悟

    学线程池的时候,我最大的收获是理解了“池化思想”和“用户态/内核态切换”的底层原理。以前只知道用线程池能提高性能,现在明白了为什么要这样设计——减少内核态切换的开销,提高资源利用率。

    另外,通过生活类比(复印店的例子),我对线程的创建、复用、拒绝有了更直观的理解。面试时,把这些原理讲清楚,再加上代码的实践,会让面试官觉得你不仅会用,还懂底层。

    3. 实习冲刺建议

    作为大二下的学生,冲刺大三上的大厂实习,线程池是必考点。建议:

    • 动手敲代码:把 ThreadPoolExecutor的构造方法、拒绝策略、线程工厂都敲一遍,理解每个参数的作用。

    • 结合项目:如果项目中有用到线程池,一定要能讲清楚为什么用、怎么配置的、遇到过什么问题。

    • 看源码:有时间的话,看看 ThreadPoolExecutor的源码,理解它的实现细节(比如线程的创建、回收、任务的执行流程)。

    赞(0)
    未经允许不得转载:171主机测评 » 12.深入理解Java线程池:从底层原理到面试通关秘籍
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址