Java 线程池:从参数到拒绝策略
Java 线程池从参数到拒绝策略目录为什么需要线程池线程池核心参数任务提交流程拒绝策略Executors 的几个预设线程池execute 和 submit 的区别小结为什么需要线程池new Thread()每次调用都会创建一个新线程。创建线程要分配栈空间、要向操作系统申请资源一个线程默认占 1MB 核外内存。如果请求量变大线程数随之暴涨内存就有可能扛不住CPU 忙着调度线程而不是执行业务逻辑直接内存溢出了。所以需要线程池来管理线程提前创建好一批线程有任务来了就分配一个去执行执行完把线程还回来下一个任务继续用。线程池核心参数ThreadPoolExecutor是 Java 线程池的核心类构造函数有七个参数publicThreadPoolExecutor(intcorePoolSize,// 核心线程数intmaximumPoolSize,// 最大线程数longkeepAliveTime,// 临时线程存活时间TimeUnitunit,// 时间单位BlockingQueueRunnableworkQueue,// 任务队列ThreadFactorythreadFactory,// 线程工厂RejectedExecutionHandlerhandler// 拒绝策略)逐个来看。corePoolSize核心线程数线程池里长期存活的线程数量。对应后厨的正式厨师不管忙不忙都在岗。默认情况下即使核心线程空闲也不会被回收除非设置了allowCoreThreadTimeOut(true)。maximumPoolSize最大线程数线程池最多能容纳的线程数。核心线程全忙、队列也满了才会创建临时线程但总数不能超过这个值。对应后厨的正式厨师 临时工的上限。keepAliveTime临时线程存活时间临时线程空闲多久后被回收。饭点过了临时工没活干了等一段时间还不来新单就让他们回去。注意这个参数默认只对临时线程生效核心线程不会被回收。unitkeepAliveTime 的时间单位秒、毫秒、分钟看场景选。workQueue任务队列核心线程全忙时新任务排在哪里等待。这是一个阻塞队列常用的有三种队列类型特点适用场景LinkedBlockingQueue无界队列默认 Integer.MAX_VALUE任务量可控不想丢任务ArrayBlockingQueue有界队列需要指定容量需要控制内存宁可拒绝也不堆积SynchronousQueue不存储元素直接交给线程任务短平快不希望排队threadFactory线程工厂控制线程的创建方式。可以设置线程名、是否是守护线程等。线上排查问题时给线程起个有意义的名字比pool-1-thread-3好找得多ThreadFactoryfactorynewThreadFactory(){privateintcount0;OverridepublicThreadnewThread(Runnabler){returnnewThread(r,order-pool-count);}};handler拒绝策略线程全忙、队列也满了新来的任务怎么处理。任务提交流程当调用execute(task)提交一个任务时线程池的处理流程是这样的三个判断节点三种结果直接执行、排队等待、创建临时线程、被拒绝。任务优先排队不是优先创建临时线程。只有队列满了才会创建临时线程。拒绝策略任务被拒绝时Java 提供了四种内置策略AbortPolicy默认直接抛RejectedExecutionException异常。调用方能立刻知道任务被拒了。CallerRunsPolicy谁提交的谁执行。如果主线程提交的任务被拒绝主线程自己去执行这个任务。好处是不丢任务坏处是会阻塞主线程如果主线程是处理 HTTP 请求的线程那这个请求的响应时间会变长。DiscardPolicy静默丢弃。不抛异常任务直接消失。适用于日志采集这类场景丢几条无所谓。DiscardOldestPolicy丢弃队列里等待最久的那个任务然后重新提交当前任务。适用于只关心最新数据的场景比如实时价格推送旧的价格数据留着也没用。策略行为是否丢任务适用场景AbortPolicy抛异常否但中断流程默认选择需要快速失败CallerRunsPolicy提交者自己执行否不能丢任务可接受延迟DiscardPolicy静默丢弃是允许丢失如日志采集DiscardOldestPolicy丢弃最旧的是只关心最新数据Executors 的几个预设线程池实际开发中很少直接new ThreadPoolExecutor()Executors工厂类提供了几个预设配置newFixedThreadPool固定大小线程池。核心线程数 最大线程数没有临时线程。队列用LinkedBlockingQueue无界。适合任务量稳定、对延迟不敏感的场景。ExecutorServicepoolExecutors.newFixedThreadPool(5);它的问题在于队列无界。如果任务提交速度持续高于处理速度队列会无限增长最终 OOM。阿里开发手册明确禁止使用Executors创建线程池根源就在这个无界队列。newCachedThreadPool缓存线程池。核心线程数为 0最大线程数为Integer.MAX_VALUE相当于无限临时线程空闲 60 秒后回收。队列用SynchronousQueue不排队直接创建线程。ExecutorServicepoolExecutors.newCachedThreadPool();它的问题在于线程数不设上限。如果瞬间来了大量任务会创建大量线程有可能把系统资源耗尽。适合任务量波动大、每个任务执行时间短的场景。newSingleThreadExecutor单线程池。只有一个线程保证任务按提交顺序执行。队列同样是无界的LinkedBlockingQueue。ExecutorServicepoolExecutors.newSingleThreadExecutor();适合需要严格顺序执行的场景比如日志写入。newScheduledThreadPool定时线程池。支持定时任务和周期任务底层用DelayedWorkQueue。适合定时轮询、心跳检测这类场景。对比一下预设核心线程最大线程队列风险FixedThreadPoolNNLinkedBlockingQueue无界队列堆积导致 OOMCachedThreadPool0MAX_VALUESynchronousQueue线程暴涨导致 OOMSingleThreadExecutor11LinkedBlockingQueue无界队列堆积导致 OOMScheduledThreadPoolNMAX_VALUEDelayedWorkQueue相对安全所以生产环境一般不直接用Executors而是手动new ThreadPoolExecutor()显式指定队列容量和拒绝策略让系统行为可预期。execute 和 submit 的区别提交任务有两种方式execute和submit。// execute提交 Runnable没有返回值executor.execute(()-doSomething());// submit提交 Callable有返回值FutureStringfutureexecutor.submit(()-{returnqueryFromDB();});Stringresultfuture.get();// 阻塞等待结果区别在于execute接收Runnable没有返回值异常只能在任务内部捕获submit接收Callable返回Future对象可以通过future.get()获取结果。如果任务抛了异常get()时会抛出ExecutionException还有一个容易忽略的点submit内部把传入的Callable包装成了FutureTask然后调用的还是execute。所以submit是execute的上层封装。// submit 的简化内部实现publicFuture?submit(Runnabletask){FutureTaskVoidftasknewFutureTask(task,null);execute(ftask);// 最终还是调 executereturnftask;}怎么选需要返回值用submit不需要就用execute。如果用submit但不调future.get()异常会被吞掉任务悄悄失败排查起来很痛苦。小结线程池就是把线程的创建和回收管理起来让系统在高并发下既不浪费资源也不失控。七个参数里corePoolSize 决定常态并发能力maximumPoolSize 决定峰值承受能力workQueue 决定过载时的行为handler 决定兜底策略。生产环境别用 Executors 的预设手动 new ThreadPoolExecutor 显式指定每个参数让线程池的行为可预期、可监控、可调优。