Java线程池与并发控制的核心概念

线程池本质是“任务调度+资源节制”机制,通过有限线程、有界/无界队列和明确拒绝策略应对并发流量,不解决并发安全而防资源滥用;核心线程数指保底不回收线程数;任务优

先分配空闲核心线程而非直接入队;LinkedBlockingQueue默认无界易致内存溢出;并发控制需选对同步原语,volatile不保证原子性;CountDownLatch用于等待完成,CyclicBarrier用于协同到达;Semaphore不绑定线程,ReentrantLock必须同线程释放;拒绝策略是熔断开关,非兜底方案;自定义策略须轻量;线程池与并发控制须配合,否则单点失效全局崩塌;IO密集型宜大线程池,CPU密集型建议设为CPU核数;并发问题根源在于未厘清变量读写时机。

线程池不是“池子”,而是「任务调度+资源节制」的组合机制

很多人一上来就背 corePoolSizemaximumPoolSize,却忽略线程池本质是**用有限线程 + 有界/无界队列 + 明确拒绝策略**来对抗不可控的并发流量。它不解决并发安全问题,只解决资源滥用问题。

  • 核心线程数(corePoolSize)不是“最小线程数”,而是“保底不回收的线程数”——哪怕空闲 60 秒,只要没超 keepAliveTime,它也不会被销毁
  • 任务进队列前,线程池**先看有没有空闲核心线程**,而不是先塞队列;这点和直觉相反,但决定了高并发下是否立刻扩容
  • LinkedBlockingQueue 默认是无界队列(容量 Integer.MAX_VALUE),一旦用错,newFixedThreadPool 就会内存溢出——因为所有任务全堆在队列里,线程来不及消费

并发控制 ≠ 加锁,而是「选对同步原语」+「明确临界区边界」

synchronizedReentrantLock 是最常见动作,但真正踩坑的是:不知道什么时候该用 CountDownLatch,什么时候该用 Semaphore,甚至把 volatile 当锁用。

  • volatile 只保证可见性,不保证原子性——counter++ 即使加了 volatile 依然线程不安全
  • CountDownLatch 是“等别人做完我再走”,适合主流程等待多个异步任务完成;CyclicBarrier 是“大家一起到齐才开工”,适合多线程协作分阶段计算
  • Semaphore(1) 看似等于锁,但它不绑定线程——A 线程 acquire(),B 线程 release() 合法;而 ReentrantLock 必须同一线程 unlock()

ThreadPoolExecutor 的拒绝策略不是兜底,而是「系统熔断开关」

默认的 AbortPolicyRejectedExecutionException,看似简单,但在生产环境常导致上游重试风暴。它真正的意义是:告诉调用方“此刻已不可服务”,而非“换个方式再试”。

  • CallerRunsPolicy 表面温和(由提交线程自己执行),但可能阻塞业务线程,尤其在 Web 容器中会让请求线程卡住,引发连接超时
  • DiscardOldestPolicy 丢的是队列头任务,如果队列里全是延迟敏感任务(如定时通知),反而丢掉最该执行的
  • 自定义拒绝策略必须轻量——不能记录日志、不能远程调用,否则拒绝逻辑本身就成了新瓶颈

线程池与并发控制必须配合使用,否则单点失效即全局崩塌

比如你用 ConcurrentHashMap 存共享状态,却把更新逻辑扔进线程池执行,却不加任何同步——那只是把数据竞争从“同一时刻多个线程改 map”变成“同一时刻多个线程改 map + 多个线程读 map”,问题一点没少。

  • 线程池负责“谁来跑”,并发控制负责“怎么跑不打架”——二者缺一不可
  • 常见反模式:submit(() -> { cache.put(key, value); }),看似用了线程池,但 cache 若是普通 HashMap,照样并发写崩
  • IO 密集型任务(如 HTTP 调用)适合大线程池 + 长队列;CPU 密集型任务(如加解密)线程数建议设为 Runtime.getRuntime().availableProcessors(),再多就是上下文切换开销
线程池参数配得再准,锁加得再细,只要没想清楚“哪个变量会被谁在什么时机读写”,并发问题就会以最意想不到的方式冒出来。