学完这篇你能得到:把线程池、锁、并发容器这三块从"背 API"变成"知道在生产里怎么选、怎么避坑"的判断力,并拿到一份可直接抄改的代码模板。
第一步:线程池的七个参数,先对应到你的业务
ThreadPoolExecutor 的构造参数一共七个,别看它多,本质就是回答三个问题:常驻几个人、最多几个人、排不下怎么办。
ThreadPoolExecutor pool = new ThreadPoolExecutor(
8, // corePoolSize 常驻核心
16, // maximumPoolSize 峰值上限
60L, TimeUnit.SECONDS, // 空闲非核心线程存活时间
new ArrayBlockingQueue<>(200), // 有界队列,关键!
new ThreadFactory() { // 一定要给线程起名
private final AtomicInteger i = new AtomicInteger(1);
public Thread newThread(Runnable r) {
return new Thread(r, "order-worker-" + i.getAndIncrement());
}
},
new ThreadPoolExecutor.CallerRunsPolicy() // 兜底策略
);
执行顺序要记牢:先占满 core → 再塞队列 → 队列满了才扩到 max → 还满就走拒绝策略。很多人以为先扩容再排队,这是最常见的误解。
参数怎么定?CPU 密集任务用 核心数 + 1;IO 密集可以按 核心数 × (1 + 等待时间/计算时间) 估算,再压测微调。
注意:绝对不要用 Executors.newFixedThreadPool() 和 newCachedThreadPool()。前者队列是 LinkedBlockingQueue 且默认容量接近无界,任务堆积会 OOM;后者最大线程数是 Integer.MAX_VALUE,高并发下会疯狂创建线程直接打爆机器。手动 new 才是正解。
第二步:拒绝策略选错,等于把锅甩给用户
JDK 内置四种:AbortPolicy 抛异常(默认)、DiscardPolicy 静默丢弃、DiscardOldestPolicy 丢最老的、CallerRunsPolicy 交给调用线程执行。
后两种"丢弃"策略在生产里基本等于事故——任务悄悄没了,日志里连个影子都没有。常用的是 CallerRunsPolicy,它会阻塞提交线程形成天然背压;或者自定义策略,把任务落库/进 MQ 稍后重试。
第三步:synchronized 还是 ReentrantLock
synchronized 是 JVM 内置的,自动加锁释放锁,代码块抛异常也不会死锁,JDK 6 之后有偏向锁→轻量级锁(CAS 自旋)→重量级锁的升级路径,绝大多数场景够用且更快。
ReentrantLock 的价值在三个 synchronized 做不到的地方:
ReentrantLock lock = new ReentrantLock();
// 1. 可超时,避免无限等待
if (lock.tryLock(500, TimeUnit.MILLISECONDS)) {
try { /* 业务 */ } finally { lock.unlock(); }
}
// 2. 可中断
lock.lockInterruptibly();
// 3. 可建多个条件队列(生产者/消费者各一个)
Condition notFull = lock.newCondition();
注意:锁对象千万别用 String 常量或 Integer 这类会被缓存的包装类型,不同业务可能锁到同一个对象上;用 private final Object lock = new Object();,并且尽量别锁 this 或公开可见的对象。
第四步:读多写少,换读写锁或 StampedLock
ReentrantReadWriteLock 的规则是:读读不互斥、读写互斥、写写互斥。适合缓存这类"读远多于写"的场景。
注意:读锁不能升级成写锁(会死锁),但写锁可以降级成读锁。别在持有读锁时去 lock.writeLock().lock()。
如果只是极短的读操作,用 StampedLock 的乐观读更快:
StampedLock sl = new StampedLock();
long stamp = sl.tryOptimisticRead();
int cur = value; // 先读
if (!sl.validate(stamp)) { // 校验期间有没有被写
stamp = sl.readLock();
try { cur = value; } finally { sl.unlockRead(stamp); }
}
代价是它不可重入,逻辑不能嵌套调用。
第五步:并发容器,重点是复合操作
- ConcurrentHashMap:JDK 8 起是"CAS + 锁单个桶头节点",并发度远高于分段锁时代。不允许 null 键值。
- CopyOnWriteArrayList:写时复制整个数组,适合监听器列表这种"读多写几乎没有"的场景;写操作代价高,且迭代器是快照,读不到最新数据。
- 阻塞队列:
ArrayBlockingQueue 有界,能配合背压,推荐;LinkedBlockingQueue 不传容量就是 Integer.MAX_VALUE,等于无界;SynchronousQueue 不存元素,直接交接线程。
注意:map.get(k) == null 再 put 是典型竞态。要用原子方法:putIfAbsent、computeIfAbsent、merge。另外 computeIfAbsent 的映射函数里不要再操作同一个 map,会死锁。
第六步:把三样拼起来——分段并行统计
Map<String, Long> result = new ConcurrentHashMap<>();
List<String> keys = List.of("a", "b", "c", "d");
CountDownLatch latch = new CountDownLatch(keys.size());
for (String k : keys) {
pool.execute(() -> {
try {
result.merge(k, 1L, Long::sum); // 原子累加
} finally {
latch.countDown();
}
});
}
latch.await(3, TimeUnit.SECONDS); // 等齐或超时
线程池负责调度,ConcurrentHashMap.merge 负责线程安全的聚合,CountDownLatch 负责汇合——这三件套覆盖了日常八成并发场景。
小结
- 线程池手动
new ThreadPoolExecutor,队列必须有界,线程必须命名,拒绝策略别静默丢弃。
- 优先
synchronized;需要超时、可中断、多条件队列时再用 ReentrantLock。
- 读多写少用
ReentrantReadWriteLock 或 StampedLock;读锁不能升级为写锁。
- 并发容器的重点不是"用它",而是"用它的原子方法",别自己拼复合操作。
- 所有锁和池都要在
finally 里释放/关闭,shutdown() 之后记得等 awaitTermination。