java多线程(八)—— 线程池 📅 2026/7/15 14:03:09 一、为什么出现线程池或者说为什么出现池化技术类似于共享单车的出现思路。1用的时候不需要自己去买一辆新的不用了也不需要自己找地方卖了他放回去给别人复用就行。同理线程池可以实现随用随取不用自己创建直接拿就行用完也不用销毁放回去就行节省避免频繁创建和销毁线程的系统开销。2在线程池出现之前如果有30个任务就要分别创建30个线程但是现在线程池里核心线程数只有10个那剩下的20个任务就排队去吧类似于共享单车只有这么些你要等别人用完再用比人手一辆要节约资源。二、java如何创建线程池方式一new ThreadPoolExecutor构造方法方式二ExecutorServive和工具类Executors阿里巴巴开发手册禁用有OOM风险不能用的原因FixedThreadPool和SingleThreadExecutor使用的是无界的LinkedBlockingQueue任务队列最大长度为 Integer.MAX_VALUE,可能堆积大量的请求从而导致 OOM。CachedThreadPool使用的是同步队列SynchronousQueue, 队列里啥也不放来一个请求就创建一个线程允许创建的线程数量为 Integer.MAX_VALUE 可能会创建大量线程从而导致 OOM。ScheduledThreadPool和SingleThreadScheduledExecutor使用的无界的延迟阻塞队列DelayedWorkQueue任务队列最大长度为 Integer.MAX_VALUE,可能堆积大量的请求从而导致 OOM。实际使用中需要根据自己机器的性能、业务场景来手动配置线程池的参数比如核心线程数、使用的任务队列、饱和策略等等。我们应该显示地给我们的线程池命名这样有助于我们定位问题。三、我们可以创建三种类型的线程池newFixedThreadPool() 该方法返回一个固定线程数量的线程池。该线程池中的线程数量始终不变。当有一个新的任务提交时线程池中若有空闲线程则立即执行。若没有空闲的线程则新的任务会被暂存在一个任务队列中待有线程空闲时便处理在任务队列中的任务。newSingleThreadExecutor方法返回一个只有一个线程的线程池。若多余一个任务被提交到该线程池任务会被保存在一个任务队列中待线程空闲按先入先出的顺序执行队列中的任务。newCachedThreadPool该方法返回一个可根据实际情况调整线程数量的线程池。线程池的线程数量不确定但若有空闲线程可以复用则会优先使用可复用的线程。若所有核心线程均在工作又有新的任务提交则被放入任务队列。当任务队列也满了以后再来的任务就要创建新的线程了此时最多可创建的线程数变为maximumPoolSize。所有线程在当前任务执行完毕后将返回线程池进行复用。非核心线程用完以后先不销毁等过了指定的等待时间并且在此段时间内一直空闲再销毁问题为什么核心线程满了之后是先将任务放到阻塞队列里面而不是直接创建非核心线程去执行任务举一个饭店的小例子饭店线程池厨师线程当饭店里面来的客人太多了正常人的思维都是让客人去排队而不是直接去招厨师。其实道理是相通的创建线程也是会耗费资源不如先让任务排队再慢慢执行。然后当客人把排队队列也占满了的时候我考虑招新厨师。客人变少后先不急着裁掉新厨师等一段时间确实一直没用到新厨师再裁举个栗子我们先自定义一个线程池回答两个问题如果这个线程池接受到了 30 个比较耗时的任务这个时候线程池的状态或者说数据是怎样的在前面 30 个比较耗时的任务还没执行完成的情况下再来多少个任务会触发拒绝策略其实这就是在问你线程池的执行流程了简单的说一下就是当接收到了 30 个比较耗时的任务时10 个核心线程数都在工作剩下的 20 个去队列里面排队。这个时候和最大线程数是没有关系的所以和线程存活时间也就没有关系。其实你知道这个线程池最多能接受多少任务你就知道这个题的答案是什么了上面的线程池中最多接受 1000队列长度 30最大线程数 1030 个任务。所以当已经接收了30个任务的情况下如果再来 1000 个比较耗时的任务这个时候队列也满了最大线程数的线程也都在工作这个时候线程池满载了。因此在前面 30 个比较耗时的任务还没执行完成的情况下再来 1001 个任务第 1001 个任务就会触发线程池的拒绝策略了。四、线程池的7大参数/** * 用给定的初始参数创建一个新的ThreadPoolExecutor。 */ public ThreadPoolExecutor(int corePoolSize,//线程池的核心线程数量 int maximumPoolSize,//线程池的最大线程数 long keepAliveTime,//当线程数大于核心线程数时多余的空闲线程存活的最长时间 TimeUnit unit,//时间单位 BlockingQueueRunnable workQueue,//任务队列用来储存等待执行任务的队列 ThreadFactory threadFactory,//线程工厂用来创建线程一般默认即可 RejectedExecutionHandler handler//拒绝策略当提交的任务过多而不能及时处理时我们可以定制策略来处理任务 )核心线程数corePoolSize核心线程数定义了工作队列满之前可以同时运行的线程数量。最大线程数maximumPoolSize:当队列中存放的任务达到队列容量的时候当前可以同时运行的线程数量变为最大线程数。newFixedThreadPool() 核心线程数最大线程数newSingleThreadExecutor核心线程数最大线程数1newCachedThreadPool工作队列workQueue:当新任务来的时候会先判断当前运行的线程数量是否达到核心线程数如果达到的话新任务就会被存放在队列中。ArrayBlockingQueue ——有限容量队列行为任务填满队列后若仍有新任务且池中线程数小于 maximumPoolSize则创建新线程若队列满且线程数已达maximumPoolSize触发拒绝策略。生产上最常用举例new ThreadPoolExecutor(2, 4, 60L, TimeUnit.SECONDS,new ArrayBlockingQueue(100));LinkedBlockingQueue —— 无限容量队列行为队列无限长请求可以无限堆积永远不会创建超过 corePoolSize 的线程maximumPoolSize 参数在此场景下无效也不会触发拒绝策略但可能撑爆内存。SynchronousQueue —— 零容量队列行为队列里一个请求也不存来了请求直接创建新线程如果已达最大线程数触发拒绝策略。用于任务短高并发任务到达速率与处理速率匹配。 美团的技术团队的文章里就说过“比如说用户要查看一个商品的信息这个结果响应的越快越好如果一个页面半天都刷不出用户可能就放弃查看这个商品了所以应该不设置队列去缓冲并发任务而是调高corePoolSize和maxPoolSize去尽可能创造多的线程快速执行任务”PriorityBlockingQueue —— 无限容量但优先级队列行为任务必须实现 Comparable 或构造时提供 Comparator优先级高的先执行意思就是可以插队呗。无界特性导致任务无限堆积同样无法创建超过 corePoolSize 的线程存活时间keepAliveTime:当线程池中的线程数量大于corePoolSize的时候如果这时没有新的任务提交核心线程外的线程不会立即销毁而是会等待直到等待的时间超过了keepAliveTime才会被回收销毁unit:keepAliveTime参数的时间单位。threadFactory:executor 线程工厂用来创建新线程。handler:4种拒绝策略AbortPolicy 直接丢弃但是抛出异常。这样我catch异常以后可以打日志也可以封装一下返给用户的信息比如淘宝返回“系统开小差”CallerRunsPolicy 不丢弃但是线程池不负责执行这个任务而是将任务回退给调用者使用调用者的线程来执行任务DiscardPolicy直接丢弃任务连异常都不抛出。意思是扔掉的不会返回任何业务上提供的信息例如“淘宝开小差了”而是直接返回503之类的HTTP错误DiscardOldestPolicy将任务队列队首的元素抛弃然后把新任务入队但貌似必填的参数只有5个其他两个有默认值点进去看发现默认的拒绝策略是AbortPolicy也就是直接丢弃任务并抛出异常给用户看一眼。五、线程池参数如何设计5.1 核心线程数一句话总结线程数大小由核数、CPU密集程度、IO密集程度三者共同决定。太大太小都不好但是设置为多少是由程序员决定的。如果线程池中线程太多线程来回切换上下文切换浪费CPU资源。而且一个java程序不能开太多线程因为操作系统分配给JVM的内存有最大限制比如说操作系统最多分配给JVM 4GB的内存你开多了超过了4GB就会发生OOM然后程序卡死。如果线程池中线程太少任务堆积来不及处理容易发生OOM。有一个简单并且适用面比较广的公式CPU 密集型任务(N1)这种任务消耗的主要是 CPU 资源可以将线程数设置为 NCPU 核心数1。比 CPU 核心数多出来的一个线程是为了防止线程偶发的缺页中断或者其它原因导致的任务暂停而带来的影响。一旦任务暂停CPU 就会处于空闲状态而在这种情况下多出来的一个线程就可以充分利用 CPU 的空闲时间。I/O 密集型任务(2N)这种任务应用起来系统会用大部分的时间来处理 I/O 交互而线程在处理 I/O 的时间段内不会占用 CPU 来处理这时就可以将 CPU 交出给其它线程使用。因此在 I/O 密集型任务的应用中我们可以多配置一些线程具体的计算方法是 2N。 拓展一下参见issue#1737线程数更严谨的计算的方法应该是最佳线程数 NCPU 核心数∗1WT线程等待时间/ST线程计算时间其中WT线程等待时间线程运行总时间 - ST线程计算时间。线程等待时间所占比例越高需要越多线程。线程计算时间所占比例越高需要越少线程。我们可以通过 JDK 自带的工具 VisualVM 来查看WT/ST比例。8核的CPU最多同时处理8个线程如果是9个线程并发的话那其实有俩线程共用同一个核对于这俩来说看似是并行实则是并发。那这俩共用同一个核的线程岂不是跟串行执行一样了吗不是的不是的因为任务又分IO密集型和CPU密集型如果他俩都是纯CPU任务的话那确实耗时跟串行一样比如任务A耗时200ms任务B耗时300ms总耗时就是500ms。但如果任务A需要IO的话假如200ms里有50ms是等待IO的这50ms就可以用来执行任务B这样总耗时就是450ms。5.2 最大线程数在绝大多数高并发/中间件场景如 Tomcat、Dubbo、Netty中官方配置都建议将 corePoolSize 和 maximumPoolSize 设为相同的值即固定大小线程池。因为最大线程数用到的时候CPU已经满载了队列也满了这时候再开几个线程去处理并不会变快反而会因为CPU切换导致变慢所以最大线程数的设计思想是——防止系统在被突发流量击垮时至少还能再硬撑几秒给监控系统报警或自动扩容留出时间无界队列适用于不允许抛弃任何一个请求的业务但这个业务又不能并发量太大否则会OOM。比如打日志不允许丢任何一份日志。空队列适合于不让用户有丝毫等待并且请求可以扔掉的业务。要么马上算要么马上报错绝不允许卡在队列里等待。并且任务量小很快就能处理完因为不能来了100个任务然后我扔掉50个。比如网关。有界队列介于有界和无界二者之间但这类业务场景最常出现。不像无界队列一样不能扔用户的抢票请求我可以给他拒绝掉也不像空队列一样任务量极小处理快并且抢票这种业务场景要求先来先得的公平性。所以就用有界队列。暂时无法处理的去排队用户那边显示转圈等待要是队列也满了最大线程也满了此时用户发来的抢票请求就直接拒绝掉类似于用户那边收到“稍后再看”。