Java多线程入门(下):线程池——线程的"现代化工厂"
想象一下:你开了一家快递公司。如果每来一个包裹就招一个快递员,等包裹送完就辞退——那公司迟早破产。正确的做法是:养一支固定的快递员队伍,来活了就分配,没活就待命。这就是线程池的核心思想。
一、什么是线程池?
线程池(Thread Pool)是一种可以复用线程的技术。
如果没有线程池,每次有任务就new Thread(),任务跑完线程就销毁。当请求量很大时,不断创建和销毁线程会带来巨大的开销,服务器甚至会因为资源耗尽而崩溃。
线程池的做法是:提前创建好一批线程放在"池子"里,有任务来了就从池子里取一个线程去执行,执行完不销毁,继续待命。
💡线程池 vs 手动创建线程
- 手动创建线程:来一个任务招一个工人,做完辞退。成本高、效率低。
- 线程池:养一支固定队伍,循环利用。成本低、可控性强。
线程池的任务队列只能放Runnable和Callable对象。
二、创建线程池:ThreadPoolExecutor
在 Java 中,创建线程池最标准的方式是使用ThreadPoolExecutor的构造方法。它一共有 7 个参数,乍一看很多,但理解了每个参数的含义,配置起来并不难。
7个参数详解
publicThreadPoolExecutor(intcorePoolSize,// 核心线程数intmaximumPoolSize,// 最大线程数longkeepAliveTime,// 临时线程存活时间TimeUnitunit,// 存活时间单位BlockingQueue<Runnable>workQueue,// 任务队列ThreadFactorythreadFactory,// 线程工厂RejectedExecutionHandlerhandler// 拒绝策略)参数一:corePoolSize(核心线程数)
含义:线程池中常驻的"正式员工"数量。即使它们空闲,也不会被回收(除非设置了allowCoreThreadTimeOut)。
常用值:
- CPU 密集型任务(大量计算):
CPU 核数 + 1 - IO 密集型任务(网络请求、文件读写):
CPU 核数 * 2
💡如何查看 CPU 核数?
Runtime.getRuntime().availableProcessors()
参数二:maximumPoolSize(最大线程数)
含义:线程池允许创建的最大线程总数(核心线程 + 临时线程)。当任务队列满了,且当前线程数小于此值时,会创建"临时工"来应急。
常用值:通常与corePoolSize相同,或 IO 密集型场景下设为 CPU 核数的 2~4 倍。
参数三:keepAliveTime(临时线程存活时间)
含义:非核心线程(临时工)在空闲状态下能存活的最大时长。超过这个时间没活干,就会被销毁以节省资源。
常用值:60(秒)。
参数四:unit(时间单位)
含义:配合keepAliveTime使用,指定存活时间的计量单位。
常用值:TimeUnit.SECONDS。
参数五:workQueue(任务队列)
含义:存放等待执行任务的缓冲池。
常用值:new LinkedBlockingQueue<>(1000)(容量视业务而定)。
⚠️强烈建议使用有界队列(指定容量),防止任务无限堆积导致内存溢出(OOM)。
参数六:threadFactory(线程工厂)
含义:用于创建新线程的工厂。主要作用是给线程起个有意义的名字(方便排查问题),以及设置守护线程状态。
常用值:Executors.defaultThreadFactory()或自定义实现。
参数七:handler(拒绝策略)
含义:当线程池和队列都满了时的"兜底方案"——新任务来了该怎么处理?
| 策略 | 说明 |
|---|---|
AbortPolicy | 丢弃任务并抛出RejectedExecutionException异常。这是默认策略。 |
DiscardPolicy | 丢弃任务,但不抛异常。不推荐,因为静默丢任务很难排查。 |
DiscardOldestPolicy | 丢弃队列里等待最久的那个老任务,然后尝试把当前任务加进去。 |
CallerRunsPolicy | 由提交任务的线程(通常是主线程)亲自执行这个任务。 |
💡CallerRunsPolicy 的妙处:它不丢任务也不报错,只是会让提交者慢下来,起到一种简单的限流作用。
完整代码演示
packageduoxiancheng;importjava.util.concurrent.*;publicclassThreadPoolDemo{publicstaticvoidmain(String[]args){// 创建一个线程池ThreadPoolExecutorpool=newThreadPoolExecutor(3,// 核心线程数:3个正式工5,// 最大线程数:最多5个人(3个正式工 + 2个临时工)60,// 临时工空闲60秒后被辞退TimeUnit.SECONDS,newLinkedBlockingQueue<>(100),// 任务队列:最多缓冲100个任务Executors.defaultThreadFactory(),// 默认线程工厂newThreadPoolExecutor.CallerRunsPolicy()// 拒绝策略:让主线程自己干);// 提交 10 个任务for(inti=1;i<=10;i++){inttaskId=i;pool.execute(()->{System.out.println("任务 "+taskId+" 正在执行,执行线程:"+Thread.currentThread().getName());try{Thread.sleep(1000);// 模拟任务耗时}catch(InterruptedExceptione){e.printStackTrace();}});}// 关闭线程池(不再接受新任务,等已有任务执行完)pool.shutdown();}}线程池的常用方法
| 方法 | 说明 |
|---|---|
execute(Runnable) | 提交一个 Runnable 任务,无返回值。 |
submit(Callable<T>) | 提交一个 Callable 任务,返回Future<T>,可以获取执行结果。 |
shutdown() | 优雅关闭:不再接受新任务,但会等队列里已有的任务执行完。 |
shutdownNow() | 强制关闭:尝试中断所有正在执行的线程,返回队列中未执行的任务列表。 |
// submit() 示例:提交 Callable 并获取结果Future<Integer>future=pool.submit(()->{return1+2+3;});Integerresult=future.get();// 阻塞等待结果System.out.println("计算结果:"+result);线程池的执行流程
当有任务提交时,线程池按以下规则处理:
- 核心线程数未满:创建新线程执行任务;
- 核心线程已满:任务进入任务队列排队;
- 队列已满:创建临时线程(不超过 maximumPoolSize);
- 临时线程也满了:触发拒绝策略。
💡关键结论:只有当"核心线程都在忙" + “任务队列满了” + “当前线程数 < maximumPoolSize” 时,才会创建临时线程。只有当"所有线程都在忙" + “任务队列也满了” 时,才会拒绝任务。
三、Executors 工具类:看起来方便,实则埋雷
Java 还提供了一个工具类Executors,可以一行代码创建线程池:
ExecutorServicepool1=Executors.newFixedThreadPool(10);// 固定线程数ExecutorServicepool2=Executors.newCachedThreadPool();// 可缓存线程池ExecutorServicepool3=Executors.newSingleThreadExecutor();// 单线程ExecutorServicepool4=Executors.newScheduledThreadPool(10);// 定时任务线程池看起来很方便?但阿里巴巴《Java开发手册》明确规定:禁止使用 Executors 创建线程池。
为什么?
因为它们底层调用的还是ThreadPoolExecutor,但参数配置有严重隐患:
| 方法 | 隐患 |
|---|---|
newFixedThreadPool | 任务队列是LinkedBlockingQueue,容量是Integer.MAX_VALUE,相当于无界队列。任务无限堆积会导致OOM。 |
newSingleThreadExecutor | 同上,队列无界,会导致OOM。 |
newCachedThreadPool | 允许创建的线程数是Integer.MAX_VALUE,线程无限创建会导致OOM。 |
newScheduledThreadPool | 同上,允许线程数无上限,会导致OOM。 |
正确做法
始终使用new ThreadPoolExecutor(...)手动指定参数,尤其是队列一定要有界:
// ✅ 正确示范:手动创建,队列有界ThreadPoolExecutorpool=newThreadPoolExecutor(4,8,60,TimeUnit.SECONDS,newArrayBlockingQueue<>(1000),// 有界队列!newThreadPoolExecutor.CallerRunsPolicy());写在最后
线程池是多线程编程的"标配"。记住这几个核心结论:
- 线程池的核心价值:复用线程,减少创建/销毁的开销,控制并发数量;
- ThreadPoolExecutor 有 7 个参数,重点是核心线程数、最大线程数、任务队列和拒绝策略;
- 任务队列一定要用有界队列,防止 OOM;
- 拒绝策略推荐
CallerRunsPolicy,既能限流又不丢任务; - 不要使用
Executors快捷方法,它们的隐藏风险在生产环境中极其危险。