刚开始学 Java 多线程的时候我写代码都是直接new Thread(() - {...}).start()。觉得挺方便的写一个新线程就完事了。后来看别人的代码才发现人家根本不这么写——都是用线程池。我第一反应是不就执行个任务吗搞那么复杂干嘛后来踩了几个坑才明白线程池这东西不是用来耍酷的。这篇依然是我的学习笔记把线程池怎么用、为什么用、有哪些坑按照我的理解捋一遍。线程池到底是干嘛的线程池的核心作用用简单的话来说就是两件事复用线程和控制并发数。先说复用。每次new Thread()创建一个线程用完了就扔掉——这其实挺浪费的。创建线程需要系统分配资源销毁线程也要回收资源频繁地创建和销毁对性能是有影响的。线程池的做法是先把一些线程创建好放着有任务就丢给它们去执行执行完线程不销毁等着下一个任务。再说控制并发数。如果一瞬间来了几千个请求每个请求都 new 一个线程系统可能直接就崩了。线程池可以限制同时跑的线程数量多的任务排队等着起到一个限流的作用。最简单的用法Executors 工具类Java 提供了一个叫Executors的工具类一行代码就能创建线程池。适合拿来快速验证一下想法。// 创建一个固定有 3 个线程的线程池ExecutorServicefixedThreadPoolExecutors.newFixedThreadPool(3);然后你就可以往里面丢任务了。丢任务有两种方式我一开始经常搞混。丢任务的方式一execute——只管做不管结果for(inti0;i5;i){inttaskNumi;fixedThreadPool.execute(()-{System.out.println(线程 Thread.currentThread().getName() 在处理任务 taskNum);try{Thread.sleep(1000);}catch(InterruptedExceptione){Thread.currentThread().interrupt();}});}5 个任务但线程池只有 3 个线程。所以会先跑 3 个有线程空闲下来了再跑剩下的 2 个。你跑一下就能看到输出的线程名是重复的——就那 3 个线程在反复用。丢任务的方式二submit——做完把结果带回来submit()可以传Callable进去Callable 和 Runnable 的区别就是它有返回值。FutureIntegerfuturefixedThreadPool.submit(()-{Thread.sleep(500);return100200;});Integerresultfuture.get();// 这里会阻塞等任务完成System.out.println(结果: result);Future有点像一个快递单号——你提交任务之后拿到一个单号然后用get()去取结果。如果任务还没做完get()会阻塞在那里等。用完要关掉fixedThreadPool.shutdown();这个我一开始老是忘。不关的话线程池里的线程会一直活着程序关不掉。这是一个很容易踩的坑。Executors 的一个大坑学到这里我觉得挺好的Executors 用起来挺简单的。但后来看到一句话让我警觉了——Executors 创建的线程池可能会引发 OOM内存溢出。怎么回事呢newFixedThreadPool内部用的是无界队列——就是队列不设上限。如果任务提交速度比处理速度快任务会在队列里越堆越多最后把内存撑爆。newCachedThreadPool更狠——它会不停地创建新线程如果任务太多线程数也会爆炸。所以 Executors 适合简单场景、自己测试的时候用。生产环境一般不用它而是手动配 ThreadPoolExecutor。生产环境建议手写 ThreadPoolExecutor手动配参数看着比 Executors 啰嗦但每个参数都在你的控制之下。intcpuCoresRuntime.getRuntime().availableProcessors();// 获取 CPU 核心数ThreadPoolExecutorthreadPoolnewThreadPoolExecutor(cpuCores*2,// 核心线程数cpuCores*4,// 最大线程数60L,// 空闲线程存活时间秒TimeUnit.SECONDS,// 时间单位newArrayBlockingQueue(100),// 阻塞队列容量 100Executors.defaultThreadFactory(),// 线程工厂newThreadPoolExecutor.AbortPolicy()// 拒绝策略);这里面的参数一个个说。核心线程数和最大线程数核心线程数是线程池一直保持的线程数量没事的时候也不会销毁。最大线程数是线程池最多能有多少个线程。那什么时候线程数会超过核心线程数呢当核心线程都在忙而且阻塞队列也满了的时候。这时候线程池会创建新的线程直到达到最大线程数。阻塞队列核心线程在忙的时候新来的任务先放到队列里排队。ArrayBlockingQueue(100)意思是队列最多能放 100 个任务。队列的大小是需要根据业务来评估的——太长了容易 OOM太短了容易触发拒绝策略。拒绝策略队列满了线程也达到最大数了再有新任务来怎么办这时候就触发拒绝策略了。Java 内置了四种AbortPolicy默认直接抛异常。如果你的业务要求任务不能丢这个策略意味着你要自己去 catch 异常处理。CallerRunsPolicy谁提交的任务谁自己去做。比如主线程丢了一个任务进去如果线程池满了主线程就自己跑这个任务。这其实起到了一个削峰的作用——任务太多的时候提交任务的线程也被拉来干活了自然就不会再疯狂提交了。DiscardPolicy静默丢弃不抛异常。丢了就丢了你都不知道。一般情况下不建议用这个。DiscardOldestPolicy把队列里最旧的任务丢掉然后尝试把新任务加进去。我个人的感觉是核心业务一般用 AbortPolicy然后做好异常处理。需要限流保护的场景可以选 CallerRunsPolicy。线程数的设置是个经验问题这个我之前一直很纠结——到底设多少个线程合适看过一些资料后大概有了一个方向CPU 密集型任务比如大量计算线程数不宜太多。一般设为 CPU 核心数 1。因为这种任务 CPU 一直在跑线程多了反而在切换上浪费时间。I/O 密集型任务比如读写文件、网络请求线程大部分时间在等待可以设多一些。通常是 CPU 核心数的 2 倍。但这些都不是绝对的。我看到的建议是先按经验设一个值然后压测看性能数据再调。没有一次到位的公式。submit 和 execute 的另一个区别除了一个能返回结果一个不能它们在异常处理上也有区别。execute()如果任务里抛了异常线程会终止异常交给未捕获异常处理器去处理。而submit()的异常会被封装到Future里等你调get()的时候才抛出来。Future?futurethreadPool.submit(()-{inti1/0;// 模拟异常});try{future.get();}catch(ExecutionExceptione){System.out.println(异常原因: e.getCause());}这个设计其实挺有用的。如果你用submit()异常可以集中处理不会让工作线程莫名其妙就挂了。优雅关闭shutdown()不是马上把线程全干掉——它是告诉线程池别再收新任务了已经在跑的让它跑完然后等所有任务完成后再关闭。还有一个shutdownNow()这个比较暴力——尝试中断正在执行的任务然后返回还没开始执行的任务列表。一般来说用shutdown()就够了。学线程池的时候我最大的感受是光看参数是记不住的一定要自己写代码跑一跑。你写一个newFixedThreadPool(2)然后往里丢 5 个任务看到输出里线程名确实只有两个在重复出现——那一瞬间就理解复用是什么意思了。这篇依然是边学边写的。如果有哪里理解得不对欢迎指出来我改。我自己的一个小习惯每次学一个新东西先写一个最小可运行的例子跑一遍再去看原理。不然原理看了半天空对空根本记不住。