首页 / 资讯中心 / 文章详情

Java 线程池详解:7 个参数怎么配、4 种拒绝策略怎么选(附手写实现)

Java 线程池详解:7 个参数怎么配、4 种拒绝策略怎么选(附手写实现) ★ FEATURED ARTICLE
Java 线程池详解7 个参数怎么配、4 种拒绝策略怎么选附手写实现 先说结论线程池就解决一个问题别每次都new Thread复用一批线程来执行任务。用它的三个理由用数据说话我实测了 1000 个任务方式总耗时单个任务开销new Thread创建并启动189 ms约 189 微秒线程池复用执行8 ms约 8 微秒差了 20 多倍。因为创建线程要分配栈内存默认 512KB~1MB、向操作系统注册成本很高。本文按会用 → 会配 → 知道原理的顺序讲。只想知道怎么用看第一、二章就够了。手写实现放第五章标注可选。一、怎么用三种创建方式1.1 方式一Executors工厂简单但不推荐生产用// 固定大小线程池ExecutorServicepoolExecutors.newFixedThreadPool(8);// 单个线程保证任务顺序执行ExecutorServicesingleExecutors.newSingleThreadExecutor();// 带缓存的线程池线程数按需增长ExecutorServicecachedExecutors.newCachedThreadPool();// 定时任务线程池这个可以放心用ScheduledExecutorServicescheduledExecutors.newScheduledThreadPool(4);提交任务// 方式 1execute不关心返回值pool.execute(()-System.out.println(任务跑完了));// 方式 2submit拿 Future可以拿返回值FutureIntegerfuturepool.submit(()-{return11;});Integerresultfuture.get();// 阻塞等结果// 方式 3invokeAll批量提交等全部完成ListCallableStringtasksList.of(()-task1,()-task2);ListFutureStringfuturespool.invokeAll(tasks);用完要关闭pool.shutdown();// 不接受新任务执行完已有的就停pool.awaitTermination(10,TimeUnit.SECONDS);// 等它真的停⚠️shutdown()和shutdownNow()的区别shutdown()温和关闭不再接受新任务但已提交的会执行完shutdownNow()暴力关闭试图中断正在执行的任务返回还没执行的任务列表。1.2 方式二直接new ThreadPoolExecutor生产推荐ThreadPoolExecutorpoolnewThreadPoolExecutor(8,// corePoolSize核心线程数16,// maximumPoolSize最大线程数60L,TimeUnit.SECONDS,// keepAliveTime非核心线程空闲存活时间newLinkedBlockingQueue(1000),// workQueue任务队列newThreadFactory(){// threadFactory怎么创建线程privatefinalAtomicIntegerseqnewAtomicInteger(1);OverridepublicThreadnewThread(Runnabler){ThreadtnewThread(r,my-pool-seq.getAndIncrement());t.setDaemon(false);returnt;}},newThreadPoolExecutor.AbortPolicy()// handler拒绝策略);1.3 为什么阿里规约禁用Executors《阿里巴巴 Java 开发手册》明确要求不要用Executors创建线程池两个原因都是可能把内存撑爆工厂方法问题newFixedThreadPool(n)队列是LinkedBlockingQueue默认容量 Integer.MAX_VALUE约 21 亿任务堆积可能 OOMnewCachedThreadPool()最大线程数是Integer.MAX_VALUE线程数可能无限增长直接 OOMnewSingleThreadExecutor()同newFixedThreadPool队列无界newScheduledThreadPool(n)✅这个相对安全队列有界结论自己new ThreadPoolExecutor7 个参数全部显式指定。这样每个参数你都知道它是什么出了问题也能定位。二、7 个参数逐个讲重点这是全文最该记住的一章。2.1corePoolSize— 核心线程数线程池会一直保持的线程数量。即使这些线程空闲也不会被销毁除非你设置allowCoreThreadTimeOut(true)。怎么定见 2.5。2.2maximumPoolSize— 最大线程数线程池允许创建的线程上限。当核心线程都忙、队列也满了才会继续创建新线程直到达到这个上限。2.3keepAliveTimeunit— 空闲存活时间非核心线程空闲超过这个时间就会被销毁。核心线程默认不受影响。60L,TimeUnit.SECONDS// 非核心线程空闲 60 秒后回收2.4workQueue— 任务队列核心线程都忙时新任务放这儿排队。三种常用队列队列特点适用场景ArrayBlockingQueue(n)有界数组实现✅推荐能防 OOMLinkedBlockingQueue(n)有界不传参就是无界 ⚠️推荐传容量SynchronousQueue容量 0任务直接交接给线程适合线程数要快速扩容的场景PriorityBlockingQueue优先级队列无界 ⚠️任务有优先级时用⚠️最容易踩的坑new LinkedBlockingQueue()不传容量 容量Integer.MAX_VALUE无界队列。后果maximumPoolSize和拒绝策略永远不会被触发任务全堆在队列里堆积到 OOM。2.5 核心线程数怎么算面试必问经验公式CPU 密集型任务计算为主线程数 CPU 核数 1 IO 密集型任务等网络/磁盘线程数 CPU 核数 * 2 或者更大为什么 CPU 密集型是核数 1因为线程数超过核数后多出来的线程只会增加上下文切换开销没有收益。1 是为了在某线程偶发阻塞缺页等时还有线程能用。为什么 IO 密集型可以更大因为线程大部分时间在等 IO不占 CPU多开几个线程能让 CPU 更忙。// 获取 CPU 核数intcpuCoresRuntime.getRuntime().availableProcessors();真实项目里别死记公式先用公式给个初值再根据监控数据队列长度、活跃线程数、拒绝次数调整。2.6threadFactory— 线程工厂决定线程怎么创建起什么名字、是否是守护线程、优先级多少。强烈建议自己指定因为默认线程名是pool-1-thread-1这种出问题时根本不知道是哪个业务newThreadFactory(){privatefinalAtomicIntegerseqnewAtomicInteger(1);OverridepublicThreadnewThread(Runnabler){ThreadtnewThread(r,order-pool-seq.getAndIncrement());t.setDaemon(false);// 还可以设置异常处理器t.setUncaughtExceptionHandler((thread,ex)-{log.error(线程 thread.getName() 异常,ex);});returnt;}} 更好的做法线程名带上业务标识order-pool、pay-pool这样jstack一眼就能看出哪个池子出问题。2.7handler— 拒绝策略线程满了、队列也满了时怎么办。详见第三章。三、任务提交后发生了什么任务流转这是理解线程池最关键的一张图。面试常问提交一个任务线程池内部怎么处理。提交任务 │ ▼ ① 核心线程数还没满 ├─ 是 → 创建新线程执行任务 ✅ └─ 否 ↓ ② 队列还有空位 ├─ 是 → 把任务放进队列排队 ✅ └─ 否 ↓ ③ 线程数还没到 maximumPoolSize ├─ 是 → 创建新线程执行任务 ✅ └─ 否 ↓ ④ 执行拒绝策略关键点队列满之前不会扩容到 maximumPoolSize。也就是说如果队列是无界的maximumPoolSize永远用不上。3.1 实测验证我设了core2, max4, 队列容量2连续提交 6 个任务打印每次提交后的状态参数: core2, max4, queue2 提交任务1 → 线程数/活跃/队列 0/0/0 → 1/1/0 ← 创建核心线程 提交任务2 → 线程数/活跃/队列 1/1/0 → 2/2/0 ← 创建核心线程 提交任务3 → 线程数/活跃/队列 2/2/0 → 2/2/1 ← 核心满了进队列 提交任务4 → 线程数/活跃/队列 2/2/1 → 2/2/2 ← 进队列 提交任务5 → 线程数/活跃/队列 2/2/2 → 3/3/2 ← 队列满了创建非核心线程 提交任务6 → 线程数/活跃/队列 3/3/2 → 4/4/2 ← 创建非核心线程完全符合上面那张图的顺序先填核心线程 → 再填队列 → 队列满了才扩容到 max。四、4 种拒绝策略怎么选策略行为适合场景AbortPolicy默认抛RejectedExecutionException不能丢任务让调用方知道失败了CallerRunsPolicy让提交任务的线程自己执行不能丢任务、又不想报错能接受拖慢调用方DiscardPolicy静默丢弃不报错允许丢任务如日志上报DiscardOldestPolicy丢掉队列里最老的把新的加进去只关心最新任务如实时数据4.1 实测对比我设了core1, max1, 队列容量2提交 6 个任务AbortPolicy(抛异常) 执行3, 抛出异常3 CallerRunsPolicy(调用者自己跑) 执行6, 抛出异常0 DiscardPolicy(静默丢弃) 执行3, 抛出异常0 DiscardOldestPolicy(丢最老的) 执行3, 抛出异常0怎么读这个数据AbortPolicy执行 3 个1 个在跑 2 个在队列剩下3 个抛异常——调用方明确知道被拒了CallerRunsPolicy6 个全部执行——被拒的任务由提交它的线程main自己跑所以耗时越长但一个不丢DiscardPolicy只执行 3 个另外 3 个静默消失——危险在没人知道丢了DiscardOldestPolicy也只执行 3 个但丢的是队列里最老的。4.2 选型建议默认用AbortPolicy至少你知道出问题了。如果必须保任务不丢、又能接受降级用CallerRunsPolicy。⚠️生产环境建议自己实现一个拒绝策略记录日志 告警 兜底落盘别让任务悄悄消失。newRejectedExecutionHandler(){OverridepublicvoidrejectedExecution(Runnabler,ThreadPoolExecutorexecutor){log.error(线程池已满任务被拒队列大小{},executor.getQueue().size());// 兜底写入数据库 / 落地到磁盘 / 降级处理thrownewRejectedExecutionException(任务被拒绝);}}五、手写一个迷你线程池可选理解原理用标准库的ThreadPoolExecutor有 2000 多行但核心逻辑其实很简单一个阻塞队列 若干个常驻的工作线程工作线程循环从队列取任务执行。5.1 完整实现约 60 行packagedemo;importjava.util.ArrayList;importjava.util.List;importjava.util.concurrent.BlockingQueue;importjava.util.concurrent.LinkedBlockingQueue;importjava.util.concurrent.atomic.AtomicInteger;/** 迷你线程池一个阻塞队列 若干常驻工作线程 */publicclassMiniThreadPool{privatefinalBlockingQueueRunnablequeue;privatefinalListThreadworkersnewArrayList();privatefinalAtomicIntegerworkerIdnewAtomicInteger(1);privatevolatilebooleanrunningtrue;publicMiniThreadPool(intcoreSize,intqueueSize){this.queuenewLinkedBlockingQueue(queueSize);for(inti0;icoreSize;i){ThreadtnewThread(this::work,mini-worker-workerId.getAndIncrement());t.setDaemon(true);workers.add(t);t.start();}}/** 工作线程死循环不断从队列取任务执行 */privatevoidwork(){while(running){try{// take() 会阻塞直到有任务 —— 这是线程不空转的关键Runnabletaskqueue.take();task.run();}catch(InterruptedExceptione){Thread.currentThread().interrupt();break;}}}/** 提交任务 */publicvoidexecute(Runnabletask){if(!running)thrownewIllegalStateException(线程池已关闭);try{queue.put(task);}catch(InterruptedExceptione){Thread.currentThread().interrupt();}}/** 关闭唤醒所有阻塞在 take() 上的线程 */publicvoidshutdown(){runningfalse;workers.forEach(Thread::interrupt);}}5.2 跑一下MiniThreadPoolpoolnewMiniThreadPool(3,10);// 3 个线程队列容量 10for(inti1;i9;i){finalintidi;pool.execute(()-System.out.println(任务id 由 Thread.currentThread().getName() 执行));}Thread.sleep(500);pool.shutdown();实测输出9 个任务被 3 个 worker 均匀消化任务1 由 mini-worker-3 执行 任务2 由 mini-worker-1 执行 任务3 由 mini-worker-2 执行 任务4 由 mini-worker-2 执行 任务5 由 mini-worker-1 执行 任务6 由 mini-worker-3 执行 任务7 由 mini-worker-2 执行 任务8 由 mini-worker-2 执行 任务9 由 mini-worker-2 执行 线程池已关闭5.3 和真实ThreadPoolExecutor的差距手写版只是能跑真实线程池还要处理很多事能力手写版ThreadPoolExecutor核心/最大线程数分层❌ 固定✅队列满后扩容❌✅拒绝策略❌ 直接阻塞✅ 4 种可配空闲线程回收❌ 常驻✅ keepAliveTime异常处理❌ 会杀掉 worker✅ 有兜底状态监控❌✅ getActiveCount 等所以理解原理可以手写生产必须用标准库。手写的价值在于看懂take()阻塞循环这个核心其余都是工程细节。六、三个生产环境容易踩的坑6.1 坑一无界队列导致 OOM// ❌ 危险不传容量 无界队列 可能 OOMnewThreadPoolExecutor(8,16,60L,TimeUnit.SECONDS,newLinkedBlockingQueue());// ✅ 正确给容量newThreadPoolExecutor(8,16,60L,TimeUnit.SECONDS,newLinkedBlockingQueue(1000));无界队列还有更隐蔽的问题maximumPoolSize和拒绝策略永远不会生效因为队列永远不满你以为设置了 max16实际上永远只有 8 个线程在跑任务全堆在队列里。6.2 坑二submit会吞掉异常这是最坑的一个。pool.submit(()-{thrownewRuntimeException(出错了);// 异常被静默捕获了});submit()会把异常封装到Future里如果你不get()异常就永远不会被打印——任务失败了你还以为成功了。// ✅ 正确姿势 1用 execute异常会由 UncaughtExceptionHandler 处理pool.execute(()-{thrownewRuntimeException(出错了);});// ✅ 正确姿势 2submit 后必须 get()Future?fpool.submit(()-{thrownewRuntimeException(出错了);});try{f.get();// 这里会抛出 ExecutionException}catch(ExecutionExceptione){Throwablereale.getCause();// 真正的异常在这里log.error(任务执行失败,real);}6.3 坑三线程池没有监控出问题全靠猜一个生产线程池至少要监控这几个指标ThreadPoolExecutorpool...;// 定期打印/上报log.info(活跃线程{}, 池大小{}, 队列长度{}, 已完成任务{},pool.getActiveCount(),// 正在干活pool.getPoolSize(),// 当前线程数pool.getQueue().size(),// 堆积任务数 ← 最重要pool.getCompletedTaskCount()// 累计完成);关键指标队列长度。如果它持续增长说明任务来得比消费快要么加线程要么限流。七、面试常问的 7 个问题1. 为什么要用线程池答①复用线程避免频繁创建销毁的开销实测 1000 个任务new Thread 189ms vs 线程池 8ms差 20 多倍②控制并发数量防止创建过多线程拖垮系统③统一管理提供排队、拒绝、监控能力。2.ThreadPoolExecutor的 7 个参数是什么答corePoolSize核心线程数、maximumPoolSize最大线程数、keepAliveTime空闲存活时间、unit时间单位、workQueue任务队列、threadFactory线程工厂、handler拒绝策略。3. 提交一个任务后线程池内部怎么处理答① 核心线程没满 → 创建核心线程执行② 核心满了 → 任务进队列排队③ 队列也满了 → 创建非核心线程直到maximumPoolSize④ 线程也满了 → 执行拒绝策略。关键点队列满之前不会扩容到最大线程数。4. 为什么阿里规约禁止用Executors创建线程池答主要是怕 OOM。newFixedThreadPool/newSingleThreadExecutor用的是无界队列容量Integer.MAX_VALUE任务堆积会 OOMnewCachedThreadPool的最大线程数是Integer.MAX_VALUE线程无限增长也会 OOM。应该自己new ThreadPoolExecutor显式指定每个参数。5. 核心线程数怎么设置答经验公式——CPU 密集型核数 1IO 密集型核数 × 2或更大。CPU 密集型超过核数只会增加上下文切换IO 密集型因为线程多数时间在等 IO可以多开。实际应先用公式估算再根据监控队列长度、拒绝次数调整。6. 四种拒绝策略分别是什么答①AbortPolicy默认抛RejectedExecutionException②CallerRunsPolicy让提交任务的线程自己执行③DiscardPolicy静默丢弃④DiscardOldestPolicy丢弃队列里最老的任务。生产建议自定义策略记录日志 告警。7.submit和execute有什么区别答execute只能提交Runnable异常会抛出由UncaughtExceptionHandler处理submit可以提交Callable并返回Future但异常被封装在Future里不调用get()就不会知道——这是最容易埋雷的地方。八、总结记住这五条就够了用哪个—— 生产环境一律new ThreadPoolExecutor7 个参数显式指定别用Executors有无界队列/无限线程的 OOM 风险。任务怎么流转—— 核心线程 → 队列 → 扩容到 max → 拒绝策略。队列不满就不会扩容。参数怎么配—— 队列必须有界核心线程数按 CPU 密集核数1、IO 密集核数×2起步再靠监控调。拒绝策略—— 默认AbortPolicy要保任务用CallerRunsPolicy生产建议自定义记日志告警。两个隐蔽坑——submit会吞异常必须get()一定要自建线程工厂给线程起带业务标识的名字不然出问题查不出来。一句口诀有界队列 自建工厂 显式参数 监控队列长度——这四件事做到线程池基本不会出事。如果本文对你有帮助欢迎点赞 收藏 关注如果疑问或补充欢迎在评论区交流关注我后续分享更多 Java 核心知识点和实战技巧
阅读完成 · 觉得有帮助?
咨询建站