目录
*1. FixedThreadPool(固定大小线程池)**
4. ScheduledThreadPool(定时任务线程池)
(二)推荐使用的线程池:手动创建 ThreadPoolExecutor
多线程是指在一个程序(进程)中同时运行多个独立的执行路径(线程),每个线程可以并行或并发地处理不同任务,从而提高程序的执行效率和资源利用率。以下是关于多线程的详细解析:
一、核心概念
-
进程 vs 线程
-
进程:是操作系统分配资源的基本单位(如内存、文件句柄等),每个进程至少包含一个主线程。
-
线程:是进程内的最小执行单元,共享进程的资源(如内存空间、全局变量),但有独立的执行栈和寄存器状态。
-
关系:一个进程可包含多个线程,线程在进程的资源中运行,切换成本低于进程。
-
-
多线程的目标
-
提高效率:利用多核 CPU 并行处理任务(如同时下载文件、播放音乐)。
-
改善用户体验:避免单线程阻塞导致程序无响应(如 GUI 程序中,主线程处理界面,子线程处理数据加载)。
-
资源共享:线程间共享进程内存,无需复杂的 IPC(进程间通信)机制。
-
二、多线程的关键特性
-
并行与并发
-
并行:多个线程在多核 CPU 上同时执行(真正的 “同时运行”)。
-
并发:多个线程在单核 CPU 上通过时间片轮转交替执行(宏观上 “同时运行”,微观上串行)。
-
-
共享资源与竞争条件
-
共享资源:线程间共享进程的内存空间,如全局变量、对象实例等。
-
竞争条件(Race Condition):多个线程同时访问或修改共享资源时,可能导致结果不可预测(如计数器自增操作)。
-
解决方案:
-
同步机制:使用锁(Lock)、信号量(Semaphore)、原子变量等保证同一时刻只有一个线程访问资源。
-
线程安全类:使用标准库中线程安全的容器(如 Java 的
ConcurrentHashMap
)。
-
-
-
线程生命周期
-
新建(New):创建线程对象但未启动。
-
就绪(Runnable):调用
start()
方法后,等待 CPU 调度。 -
运行(Running):获取 CPU 资源,执行
run()
方法。 -
阻塞(Blocked):因等待锁、IO 操作等暂停执行。
-
终止(Terminated):线程执行完毕或异常终止。
-
三、多线程的实现方式
不同编程语言和框架提供了多种创建线程的方式,以下是常见示例:
-
1、继承
Thread
类(基础方式)核心思想:创建一个类继承
Thread
类,重写run()
方法定义线程任务,通过start()
方法启动线程。 代码示例:public class MyThread extends Thread { // 重写 run() 方法,定义线程执行逻辑 @Override public void run() { System.out.println("线程 " + getName() + " 运行中"); } public static void main(String[] args) { MyThread thread = new MyThread(); thread.setName("自定义线程"); // 设置线程名称 thread.start(); // 启动线程(而非直接调用 run()) } }
特点:
-
优点:简单直观,直接操作线程对象。
-
缺点:Java 不支持类的多继承,限制了子类的扩展性;代码与线程耦合度高,不利于资源共享。 适用场景:简单场景下的独立线程任务,无需共享状态。
2、实现
Runnable
接口(推荐方式)核心思想:创建一个类实现
Runnable
接口,重写run()
方法定义任务,将该类实例作为参数传入Thread
构造器中启动线程。 代码示例:public class MyRunnable implements Runnable { @Override public void run() { System.out.println("线程 " + Thread.currentThread().getName() + " 运行中"); } public static void main(String[] args) { Runnable task = new MyRunnable(); // 创建任务对象 Thread thread = new Thread(task); // 将任务绑定到线程 thread.setName("Runnable 线程"); thread.start(); } }
特点:
-
优点:避免单继承限制,支持资源共享(多个线程可共享同一个
Runnable
实例);分离 “线程” 与 “任务”,代码解耦性更强。 -
缺点:需显式与
Thread
类结合,代码稍复杂。 适用场景:多线程共享同一资源(如计数器、缓存)的场景,或需要继承其他类的情况。
3、实现
Callable
接口(带返回值的线程)核心思想:通过
Callable
接口和Future
机制实现有返回值的线程任务,需搭配ExecutorService
线程池使用。 代码示例:import java.util.concurrent.*; public class MyCallable implements Callable<String> { // 泛型指定返回值类型 @Override public String call() throws Exception { // 可抛出异常,支持返回值 Thread.sleep(1000); // 模拟耗时任务 return "任务完成,当前时间:" + System.currentTimeMillis(); } public static void main(String[] args) throws ExecutionException, InterruptedException { ExecutorService executor = Executors.newSingleThreadExecutor(); // 创建线程池 Callable<String> task = new MyCallable(); Future<String> future = executor.submit(task); // 提交任务并获取 Future 对象 // 获取线程执行结果(可阻塞等待或设置超时) String result = future.get(); // 阻塞直到任务完成 System.out.println("线程返回结果:" + result); executor.shutdown(); // 关闭线程池 } }
特点:
-
优点:支持线程执行结果返回和异常处理;通过线程池管理线程,避免资源浪费。
-
缺点:需结合线程池使用,代码复杂度较高。 适用场景:需要获取线程执行结果或处理异常的场景(如异步计算、任务分治)。
4、使用线程池(
ExecutorService
框架)核心思想:通过
Executor
框架管理线程池,复用线程对象,避免频繁创建 / 销毁线程的开销。不推荐直接使用(原因见下文 “推荐使用的线程池” 部分)。以下是四种常见类型:
(一)线程池的四种类型
*1. FixedThreadPool(固定大小线程池)**
-
创建方式:
ExecutorService fixedPool = Executors.newFixedThreadPool(int nThreads);
-
核心特性:
-
核心线程数 = 最大线程数 =
nThreads
,线程数固定,不会销毁。 -
任务队列是 无界队列
LinkedBlockingQueue
,任务积压时可能导致 OOM(内存溢出)。
-
-
适用场景: 长期运行的固定并发任务(如数据库连接池线程),但需注意队列长度限制。
2. CachedThreadPool(可缓存线程池)
-
创建方式:
ExecutorService cachedPool = Executors.newCachedThreadPool();
-
核心特性:
-
核心线程数为 0,最大线程数为
Integer.MAX_VALUE
(理论无上限)。 -
任务队列是 同步队列
SynchronousQueue
,无任务时线程存活时间为60s
,空闲线程会被销毁。
-
-
风险: 短时间提交大量任务时可能创建海量线程,导致 CPU 耗尽或 OOM。
-
适用场景: 少量短期异步任务(如临时计算),不适合高并发场景。
3. SingleThreadExecutor(单线程池)
-
创建方式:
ExecutorService singlePool = Executors.newSingleThreadExecutor();
-
核心特性:
-
仅包含 1 个核心线程,保证任务按顺序串行执行。
-
任务队列是 无界队列
LinkedBlockingQueue
,同样存在 OOM 风险。
-
-
适用场景: 需要保证顺序执行的任务(如单例资源操作、数据库串行写入)。
4. ScheduledThreadPool(定时任务线程池)
-
创建方式:
ScheduledExecutorService scheduledPool = Executors.newScheduledThreadPool(int corePoolSize);
-
核心特性:
-
核心线程数由
corePoolSize
指定,最大线程数为Integer.MAX_VALUE
。 -
支持定时任务(
scheduleAtFixedRate
)和周期性任务(scheduleWithFixedDelay
)。
-
-
适用场景: 定时任务(如日志刷新、缓存过期清理)、周期性任务(如心跳检测)。
(二)推荐使用的线程池:手动创建
ThreadPoolExecutor
为什么不推荐
Executors
?Executors
创建的线程池使用 无界队列(如LinkedBlockingQueue
)或 过大的最大线程数,可能导致:-
内存溢出:大量任务积压在无界队列中,耗尽内存。
-
资源耗尽:
CachedThreadPool
可能创建过多线程,导致 CPU 饱和或线程竞争加剧。
推荐方式:手动构造
ThreadPoolExecutor
通过
ThreadPoolExecutor
构造方法自定义七大参数,精准控制线程池行为:ThreadPoolExecutor executor = new ThreadPoolExecutor( corePoolSize, // 核心线程数 maximumPoolSize, // 最大线程数 keepAliveTime, // 非核心线程存活时间 unit, // 时间单位 workQueue, // 任务队列 threadFactory, // 线程工厂 handler // 拒绝策略 );
(三)线程池七大核心参数详解
1. corePoolSize(核心线程数)
-
定义:线程池中常驻的核心线程数量,即使无任务也不会销毁(除非
allowCoreThreadTimeOut
设为true
)。 -
作用:
-
任务提交时,优先创建核心线程执行任务,直到达到
corePoolSize
。 -
建议根据任务类型设置:
-
CPU 密集型:设为
CPU核心数 + 1
(充分利用 CPU,避免上下文切换)。 -
IO 密集型:设为
2 * CPU核心数
(线程可能因 IO 阻塞,需更多线程处理任务)。
-
-
2. maximumPoolSize(最大线程数)
-
定义:线程池允许创建的最大线程数量(核心线程 + 非核心线程)。
-
触发条件: 当任务队列已满且当前运行线程数 <
maximumPoolSize
时,创建新线程(非核心线程)处理任务。 -
注意:
-
非核心线程在空闲时间超过
keepAliveTime
时会被销毁。 -
若任务队列是 无界队列(如
LinkedBlockingQueue
),maximumPoolSize
形同虚设(队列不会满,不会创建非核心线程)。
-
3. keepAliveTime(存活时间)
-
定义:非核心线程在空闲状态下的存活时间,超时则销毁。
-
作用范围:
-
当
线程数 > corePoolSize
时,非核心线程空闲时间超过keepAliveTime
会被回收。 -
可通过
setKeepAliveTime()
动态调整。
-
4. unit(时间单位)
-
可选值:
TimeUnit
枚举类(NANOSECONDS
,MICROSECONDS
,MILLISECONDS
,SECONDS
等)。 -
示例:
keepAliveTime = 30
,unit = TimeUnit.SECONDS
表示非核心线程最多存活 30 秒。
5. workQueue(任务队列)
-
定义:用于存放待执行任务的队列,类型分为三类:
-
直接提交队列:如
SynchronousQueue
,无存储空间,任务直接提交给线程,队列满时创建新线程(适用于CachedThreadPool
)。 -
有界队列:如
ArrayBlockingQueue
(指定容量),队列满后触发拒绝策略(推荐使用,避免 OOM)。 -
无界队列:如
LinkedBlockingQueue
(默认容量Integer.MAX_VALUE
),可能导致内存溢出(Executors
默认使用,不推荐)。
-
-
选择建议:
-
高并发场景:优先使用 有界队列(如
ArrayBlockingQueue(1000)
),配合合理的maximumPoolSize
和拒绝策略。
-
6. threadFactory(线程工厂)
-
作用:用于创建线程的工厂,可自定义线程名称、优先级、是否为守护线程等。
-
最佳实践:
ThreadFactoryBuilder
(如 Guava 库)命名线程,便于故障排查:
ThreadFactory namedFactory = new ThreadFactoryBuilder() .setNameFormat("my-thread-pool-%d").build();
7. handler(拒绝策略)
-
触发条件:当任务队列已满且线程数达到
maximumPoolSize
时,新任务会被拒绝,需通过拒绝策略处理。 -
四种内置拒绝策略:
-
AbortPolicy(默认):直接抛出
RejectedExecutionException
,需调用方捕获处理。 -
CallerRunsPolicy:由提交任务的主线程执行任务(降低提交速度,适用于非异步场景)。
-
DiscardPolicy:静默丢弃无法处理的任务(可能丢失数据,需谨慎)。
-
DiscardOldestPolicy:丢弃队列中最旧的任务,尝试提交新任务(存在数据丢失风险)。
-
-
自定义策略: 实现
RejectedExecutionHandler
接口,自定义处理逻辑(如记录日志、写入数据库重试等)。
(四)线程池参数配置最佳实践
-
避免使用无界队列:优先选择有界队列(如
ArrayBlockingQueue
),防止内存溢出。 -
合理设置核心线程数:
-
通过
Runtime.getRuntime().availableProcessors()
获取 CPU 核心数。 -
CPU 密集型:
corePoolSize = CPU核心数 + 1
。 -
IO 密集型:
corePoolSize = CPU核心数 * 2
(或通过压测确定)。
-
-
选择合适的拒绝策略:
-
关键业务:使用
AbortPolicy
并捕获异常,记录日志或通知管理员。 -
非关键业务:可使用
DiscardPolicy
或自定义重试逻辑。
-
-
监控线程池状态: 通过
getQueue().size()
、getActiveCount()
等方法监控队列长度和线程活跃数,动态调整参数。
(五)总结
线程池类型 核心场景 是否推荐 风险提示 FixedThreadPool 固定并发任务(如数据库连接) 否 无界队列可能导致 OOM CachedThreadPool 少量短期任务(如临时计算) 否 可能创建海量线程导致 CPU 耗尽 SingleThreadExecutor 顺序执行任务(如单例资源操作) 否 无界队列风险 ScheduledThreadPool 定时 / 周期性任务(如缓存清理) 否 需注意最大线程数限制 手动创建 ThreadPoolExecutor 所有生产环境场景(自定义参数) 是 需根据业务精准配置七大参数 核心原则:线程池的配置没有 “银弹”,需结合业务特点(任务类型、并发量、数据重要性)和硬件资源(CPU、内存)综合调优,优先使用有界队列和合理的拒绝策略,避免系统崩溃。
-
四、多线程的优缺点
优点:
-
提高 CPU 利用率,适合 IO 密集型任务(如网络请求、文件读写)。
-
善程序响应速度,避免主线程阻塞。
-
简化编程模型,可通过线程拆分复杂任务。
缺点:
-
线程安全问题复杂,需额外处理共享资源竞争。
-
线程切换带来上下文开销(尤其线程数量过多时)。
-
调试和测试难度大,难以复现偶发的竞态条件。
五、适用场景
-
IO 密集型任务:如网络爬虫(多个线程同时下载网页)、文件处理(多线程读写不同文件)。
-
实时交互程序:GUI 应用(主线程处理界面,子线程处理数据加载)。
-
任务拆分:将复杂计算拆分为多个子任务并行处理(如视频渲染、大数据分析)。
-
服务器应用:多线程处理客户端请求(如 Web 服务器的线程池模型)。
六、注意事项
-
避免过度使用线程:线程数量过多会导致内存占用过高、上下文切换频繁,反而降低性能。
-
线程安全设计:对共享资源的操作必须加锁或使用原子操作,确保数据一致性。
-
合理使用线程池:通过线程池复用线程,避免频繁创建 / 销毁线程的开销(如 Java 的
ExecutorService
、Python 的concurrent.futures
)。 -
处理线程阻塞:避免线程长时间阻塞(如死锁、无限制等待),可设置超时机制。