并发编程二:线程池、JUC 工具与异步编程
- 为什么不要频繁
new Thread; - 线程池核心参数怎么理解;
- 线程池任务执行流程是什么;
- 拒绝策略怎么选;
CountDownLatch、CompletableFuture适合什么场景;- 并发集合解决什么问题;
- Spring Boot 里怎么做异步;
- 生产环境怎么做线程池隔离。
锁和 CAS 本身展开会很长,所以单独写成专题:
为什么不要一直 new Thread
最直觉的并发写法是来一个任务就创建一个线程:
for (int i = 0; i < 10000; i++) { new Thread(() -> { System.out.println("处理订单"); }).start();}这种写法在 Demo 里能跑,但在真实项目里很危险。
问题主要有三个:
- 线程创建和销毁有成本;
- 线程太多会造成大量上下文切换;
- 请求量上来后可能把内存和 CPU 打满。
线程不是越多越快。CPU 核数是有限的,线程过多时,系统会花大量时间在线程切换上,真正执行任务的时间反而变少。
所以生产项目里更常见的做法是使用线程池。
线程池解决什么问题
线程池的核心价值是复用线程和控制资源。
可以把它理解成:
提前准备一批工作线程任务来了放进去有空闲线程就执行忙不过来就排队再忙不过来就拒绝这样做的好处是:
- 避免频繁创建和销毁线程;
- 控制最大并发量;
- 让任务可以排队;
- 在系统扛不住时触发拒绝策略,而不是无限制拖垮机器。
不推荐直接使用 Executors
很多入门示例会这样创建线程池:
ExecutorService pool = Executors.newFixedThreadPool(10);这很方便,但不推荐在生产环境直接使用。
原因是 Executors 一些工厂方法内部使用的是无界队列或不受控的线程数量,容易导致任务堆积甚至 OOM。
更推荐显式创建 ThreadPoolExecutor:
ThreadPoolExecutor executor = new ThreadPoolExecutor( 5, 10, 60, TimeUnit.SECONDS, new LinkedBlockingQueue<>(100), new ThreadPoolExecutor.CallerRunsPolicy());虽然写起来长一点,但每个参数都清楚可控。
线程池核心参数
ThreadPoolExecutor 最重要的参数有这些:
| 参数 | 含义 |
|---|---|
corePoolSize | 核心线程数 |
maximumPoolSize | 最大线程数 |
keepAliveTime | 非核心线程空闲存活时间 |
unit | 时间单位 |
workQueue | 任务队列 |
threadFactory | 线程创建工厂 |
handler | 拒绝策略 |
举个订单通知线程池:
ThreadPoolExecutor notificationExecutor = new ThreadPoolExecutor( 4, 8, 60, TimeUnit.SECONDS, new ArrayBlockingQueue<>(500), runnable -> new Thread(runnable, "notification-worker"), new ThreadPoolExecutor.CallerRunsPolicy());这里的意思是:
- 平时保留 4 个核心线程;
- 高峰期最多扩到 8 个线程;
- 队列最多堆 500 个任务;
- 如果实在处理不过来,就让提交任务的线程自己执行。
线程池执行流程
线程池处理任务时,大致按这个顺序:
任务提交 ↓核心线程数是否已满? ↓ 否创建核心线程执行 ↓ 是队列是否已满? ↓ 否进入队列等待 ↓ 是最大线程数是否已满? ↓ 否创建非核心线程执行 ↓ 是执行拒绝策略注意一个容易误解的点:
线程池不是核心线程满了就直接创建非核心线程,而是先尝试进队列。
只有队列满了,才会继续创建非核心线程。
常见拒绝策略
线程池扛不住时,拒绝策略决定任务怎么处理。
常见策略:
| 策略 | 行为 |
|---|---|
AbortPolicy | 直接抛异常,默认策略 |
CallerRunsPolicy | 谁提交任务,谁自己执行 |
DiscardPolicy | 直接丢弃任务,不抛异常 |
DiscardOldestPolicy | 丢弃队列里最旧的任务 |
业务里最常见的是 AbortPolicy 和 CallerRunsPolicy。
AbortPolicy 适合希望尽快暴露问题的场景。
CallerRunsPolicy 适合做一点反压,让提交方慢下来。
不要轻易使用静默丢弃策略,否则任务丢了可能很难发现。
线程池大小怎么估算
线程池大小没有万能公式,但可以先按任务类型粗略估计。
CPU 密集型任务:
线程数 ≈ CPU 核数 + 1比如加密、压缩、复杂计算。
IO 密集型任务:
线程数 ≈ CPU 核数 * 2比如调用外部接口、读写数据库、读写文件。
但真实项目还是要结合监控调整:
- CPU 使用率;
- 队列长度;
- 任务耗时;
- 拒绝次数;
- 活跃线程数;
- 下游服务响应时间。
线程池不是配完就结束,应该持续观察和调优。
CountDownLatch:等待多个任务完成
CountDownLatch 适合一个线程等待多个任务全部完成。
比如商品详情页需要并行查询:
- 商品信息;
- 库存;
- 评论;
- 推荐商品。
示例:
CountDownLatch latch = new CountDownLatch(3);
executor.execute(() -> { try { System.out.println("查询商品"); } finally { latch.countDown(); }});
executor.execute(() -> { try { System.out.println("查询库存"); } finally { latch.countDown(); }});
executor.execute(() -> { try { System.out.println("查询评论"); } finally { latch.countDown(); }});
latch.await();System.out.println("全部加载完成");countDown() 最好放在 finally 里,否则任务异常后没有减计数,主线程可能一直等下去。
CompletableFuture:更现代的异步编程
CompletableFuture 更适合编排异步任务。
商品详情页可以这样写:
CompletableFuture<String> productFuture = CompletableFuture.supplyAsync(() -> "商品信息", executor);
CompletableFuture<String> stockFuture = CompletableFuture.supplyAsync(() -> "库存信息", executor);
CompletableFuture<String> commentFuture = CompletableFuture.supplyAsync(() -> "评论信息", executor);
CompletableFuture.allOf( productFuture, stockFuture, commentFuture).join();
String product = productFuture.join();String stock = stockFuture.join();String comment = commentFuture.join();注意这里显式传入了 executor。
不建议在业务项目里随便使用默认线程池,因为不同业务混在同一个公共线程池里,出现慢任务时很难隔离。
CompletableFuture 的异常处理
异步任务一定要考虑异常。
CompletableFuture<String> future = CompletableFuture .supplyAsync(() -> queryStock(), executor) .exceptionally(ex -> { log.error("query stock failed", ex); return "库存未知"; });如果多个异步任务里某一个失败,要想清楚:
- 是整体失败;
- 还是返回兜底数据;
- 是否需要重试;
- 是否需要记录补偿任务。
异步不是把任务丢出去就结束,失败处理才是重点。
并发集合
普通集合在并发读写时可能不安全。
比如多个线程同时写 HashMap,可能出现数据错乱。并发场景下可以使用 ConcurrentHashMap。
ConcurrentHashMap<Long, String> productCache = new ConcurrentHashMap<>();
productCache.put(1001L, "键盘");String name = productCache.get(1001L);常见并发集合:
| 集合 | 适合场景 |
|---|---|
ConcurrentHashMap | 高并发 Map 读写 |
CopyOnWriteArrayList | 读多写少的列表 |
BlockingQueue | 生产者消费者模型 |
ConcurrentLinkedQueue | 非阻塞队列 |
CopyOnWriteArrayList 适合读多写少,比如商品分类缓存、配置列表。写入时会复制数组,所以不适合高频写。
ConcurrentHashMap 内部会用到 CAS、锁分段等思想,CAS 细节可以看:Java CAS 专题:从 AtomicInteger 到乐观并发。
Spring Boot 异步
Spring Boot 里可以用 @Async 做异步任务。
这里只放最基础的用法,@Async 的线程池配置、异常处理、事务边界和常见失效场景,详细可以看:Spring @Async 实战:异步方法、线程池配置与常见失效场景。
先开启异步:
@EnableAsync@Configurationpublic class AsyncConfig {}再写异步方法:
@Async("notificationExecutor")public void sendMessage(Long userId) { System.out.println("发送短信:" + userId);}生产环境不建议直接用默认线程池,最好显式配置:
@Bean("notificationExecutor")public Executor notificationExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setCorePoolSize(4); executor.setMaxPoolSize(8); executor.setQueueCapacity(500); executor.setThreadNamePrefix("notification-"); executor.initialize(); return executor;}还要注意:同一个类内部直接调用 @Async 方法,通常不会走代理,也就不会异步执行。
线程池隔离
不要把所有异步任务都丢进同一个线程池。
错误做法:
发短信、写日志、导出报表、同步库存,全都使用同一个线程池如果导出报表占满线程池,短信和库存同步也会被拖慢。
更推荐按业务拆分:
notificationExecutor:短信、邮件、站内信exportExecutor:报表导出inventoryExecutor:库存同步logExecutor:日志写入线程池隔离的目的不是“看起来更规范”,而是防止一个慢业务拖垮所有异步任务。
总结
- 不要频繁
new Thread,生产环境优先使用线程池。 - 不推荐直接使用
Executors,更推荐显式配置ThreadPoolExecutor。 - 线程池要关注核心线程数、最大线程数、队列长度和拒绝策略。
CountDownLatch适合等待多个任务完成。CompletableFuture适合异步任务编排,但要显式指定线程池。- 并发集合不是万能的,要结合读写特点选择。
- Spring Boot 异步要配置独立线程池,并注意代理调用问题。
- 线程池要按业务隔离,避免互相拖垮。
If this article helped you, please share it with others!
Some information may be outdated






