目录
一、上节课重点回顾(温故而知新)
二、本节课重点:线程池(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的源码,理解它的实现细节(比如线程的创建、回收、任务的执行流程)。




