Skip to content

Java 并发模型总览

一句话定义

Java 并发模型是一套把任务、线程、共享内存、同步原语与内存可见性规则放进统一语义里的工程体系:你既要决定“谁执行”,也要决定“谁能看见什么、在什么时刻生效、出问题时怎么定位”。

代码索引

主题Lab 说明源码
Thread / Runnable / Callable / Future、线程池与拒绝策略thread-pool-basicsThreadPoolBasics.java
LinkedBlockingQueue / ArrayBlockingQueue / SynchronousQueuequeue-strategy-compareQueueStrategyCompare.java
volatile / AtomicInteger / synchronized 对比volatile-vs-atomic-vs-lockVolatileVsAtomicVsLock.java
单例、安全发布、DCLsingleton-dclSingletonDcl.java
HashMap / ConcurrentHashMap / 复合操作map-concurrency-compareMapConcurrencyCompare.java

前置基础(跨模块):位运算 · CAS · 红黑树 · LRU / LruCache
Java 专题CHM 速览 · map-concurrency-compare

为什么需要

对高年限工程师来说,Java 并发不是“会不会起线程”的问题,而是以下几个现实约束的交点:

  • 吞吐与时延:服务端要把 CPU、IO、队列、线程池压到合理水位;Android 要避免主线程阻塞与后台线程失控。
  • 正确性:只要有共享状态,就会遇到可见性、竞态、重复提交、缓存失效、乱序读写。
  • 资源边界:线程不是免费的。线程栈、调度切换、上下文切换、锁竞争都会直接反映在内存占用与尾延迟上。
  • 工程可维护性:真正难的不是写出并发代码,而是半年后还能解释:为什么这里用 volatile 而不是锁、为什么这个线程池不会把机器打死、为什么这个 map 不会在高并发下退化。

如果把并发仅理解为“多线程提高性能”,通常会在三个地方翻车:

  1. 把并发当并行:任务数变多不等于执行更快,尤其是 IO、锁竞争、串行瓶颈明显时。
  2. 把 API 当语义:会写 ExecutorService.submit() 不代表理解 Future.get() 形成的 happens-before。
  3. 把问题留给线上:死锁、活锁、竞态条件往往在本地很难稳定复现,但在线上会以 ANR、CPU 飙升、偶现脏数据、请求超时的形式长期存在。

底层机制

这一节现在只保留总览地图;各主题的详细说明、代码示例、选型边界与排障思路已拆到独立专题文档,避免总览过长、README 重复堆叠。

Java 专题导航

1. 任务与线程:Thread / Runnable / Callable / Future

  • Thread执行载体;直接 new Thread() 代表你在手动管理生命周期、调度成本与异常边界。
  • Runnable / Callable任务描述;前者无返回值,后者有返回值且可抛异常。
  • Future异步结果句柄,解决的是拿结果、取消、查状态,不是执行机制本身。
  • 工程上最容易踩坑的是:把 Future.get() 放在主线程、关键请求路径或线程池内部互等路径里,导致异步重新退化成阻塞。
  • 详见:Thread / Runnable / Callable / Future

2. ExecutorService:把“执行”从“创建线程”提升到“治理任务”

  • 线程池真正治理的是:任务排队、线程复用、扩容边界、拒绝策略、生命周期管理
  • 看线程池不能只看“几个线程”,而要同时看 corePoolSizemaximumPoolSizeworkQueueRejectedExecutionHandler 如何共同决定系统行为。
  • Android / App 场景往往更适合小而有界的线程池和队列,而不是无界排队或激进扩线程。
  • 详见:ExecutorService / 线程池

3. synchronized / Lock / volatile:三个层次的问题,不是一组同义词

  • synchronized:优先解决互斥 + 可见性,适合边界清晰、临界区短的场景。
  • Lock:在需要 tryLock()、可中断等待、公平策略、Condition 时提供更强控制,但复杂度更高。
  • volatile:解决可见性 + 有序性约束,不解决 count++ 这类复合操作的原子性。
  • 选型原则不是“谁更高级”,而是你到底要解决互斥、状态可见,还是复杂协调
  • 详见:synchronized / Lock / volatile

4. ConcurrentHashMap / Atomic*:高并发容器与无锁原语

  • ConcurrentHashMap 解决的是高并发共享字典问题;关键不是“线程安全版 HashMap”,而是正确使用 computeIfAbsent / putIfAbsent 等原子复合 API。
  • Atomic* / LongAdder 解决的是单变量原子更新与热点计数问题;它们适合计数器、状态位、引用切换,但不适合承载复杂事务一致性。
  • 这一层最常见误区是:容器安全 ≠ 业务流程安全,CAS 能跑 ≠ 复杂约束就能不用锁。
  • 详见:ConcurrentHashMap(CHM)速览 · Atomic* / CAS / LongAdder

5. JMM:Java Memory Model 才是并发语义的地基

  • JMM 解决的是:在编译器优化、CPU 重排序、缓存层次都存在时,多线程结果如何仍然可推理
  • 复习时重点抓三件事:可见性、原子性、有序性,以及它们通过 happens-before 如何建立推理边界。
  • 安全发布、DCL、volatile 引用替换,本质上都在处理“别人是否会看到半初始化对象”。
  • 详见:JMM:happens-before / 安全发布

6. 死锁 / 活锁 / 竞态:问题类型与定位思路

  • 死锁:线程互持资源永久等待;活锁:线程一直在跑但没有有效推进;竞态:结果依赖不可控时序。
  • 线上排查先分型:卡死看锁和线程池饥饿,CPU 高看自旋/活锁,偶发脏数据看竞态与发布边界
  • 真正高效的定位手段不是肉眼猜代码,而是线程 dump、JFR / profiler、关键时序日志与最小复现。
  • 详见:死锁 / 活锁 / 竞态定位

Android / Flutter / Web / Backend 对照

维度Java 并发模型AndroidFlutterWebBackend
执行单元Thread / pool / task主线程 + Binder/线程池/HandlerThreadisolate + event loop + Futureevent loop + task/microtask + Workerrequest thread / event loop / pool
共享状态共享内存为主跨线程共享对象很常见isolate 默认不共享内存,靠消息传递主线程 JS 单线程,Worker 间靠消息缓存、连接池、map、队列大量共享
同步原语synchronized / Lock / volatile / Atomic*同上,外加 Looper/Handler 约束不靠锁做主模型,更多靠消息隔离不靠锁做主模型,更多靠事件顺序锁、CAS、并发容器是日常
常见问题死锁、竞态、可见性、池饥饿ANR、主线程阻塞、后台争锁isolate 切换成本、消息序列化、主 isolate 卡顿长任务阻塞 UI、异步回调顺序错觉线程池打满、队列堆积、热点锁、缓存击穿
工程心智共享内存 + happens-before既要并发正确,也要 UI 时序正确以隔离换简单,以消息换共享以事件循环换线程复杂度以资源治理换吞吐和稳定性

对有 Android/Flutter/Web 背景的工程师,最关键的迁移理解是:

  • Java 并发默认是共享内存模型,不是 isolate/message passing 模型。
  • 因此它的难点不在“怎么异步”,而在“共享状态怎么证明安全”。
  • 这也是为什么在 Java 世界里,线程池参数、锁粒度、可见性边界会比语法层 API 更重要。

常见场景

1. Android 图片压缩 / 文件处理池

对应 Labthread-pool-basics 说明 · queue-strategy-compare 队列选型

这是移动端最典型的场景之一:

  • 图片压缩
  • 缩略图生成
  • 文件拷贝/解压
  • 大 JSON 本地解析

这类任务共同特点:

  • 不能放主线程
  • 任务量可能被用户连续点击放大
  • 结果通常是“有延迟可以接受,但不能把 App 拖死”

一个更现实的做法是:

  • 小而有界的线程池
  • 有界队列
  • 明确任务取消或页面退出后的善后策略

例如:

java
ThreadPoolExecutor imagePool = new ThreadPoolExecutor(
    2,
    4,
    30L,
    TimeUnit.SECONDS,
    new ArrayBlockingQueue<>(8),
    r -> new Thread(r, "img-worker"),
    new ThreadPoolExecutor.DiscardOldestPolicy()
);

这个配置背后的业务假设是:

  • 旧图片处理任务如果已经过时,可以丢掉
  • 新任务更接近用户当前操作
  • 队列上限要明确,避免页面已经退出还积压一堆历史任务

如果业务要求“每个任务都必须成功”,那这个拒绝策略就不合适,可能要换成上层排队/持久化任务方案,而不是线程池里硬扛。

2. 异步日志 / 通知写盘池

对应 Labthread-pool-basics 说明 · ThreadPoolBasics.java

这类任务通常更适合:

  • 单线程或极小线程池
  • 顺序写入
  • 明确 flush 时机

例如:

java
ExecutorService logPool = new ThreadPoolExecutor(
    1,
    1,
    0L,
    TimeUnit.SECONDS,
    new ArrayBlockingQueue<>(200),
    r -> new Thread(r, "log-writer"),
    new ThreadPoolExecutor.DiscardPolicy()
);

为什么这样配:

  • 单线程:避免并发写盘造成顺序混乱
  • 小有界队列:不让日志无限堆积吃内存
  • DiscardPolicy:在极端高峰下,宁可丢低价值日志,也不影响主流程

但前提是:

  • 你明确接受“部分日志丢失”
  • 核心交易、支付、审计日志不能这么干

3. 后台任务调度(泛化场景)

  • 服务端批处理、异步通知、索引构建
  • Android 本地数据库、解压、图片处理、日志落盘
  • 核心关注:线程池隔离、拒绝策略、超时与取消传播

2. 共享缓存与去重

  • ConcurrentHashMap 做本地缓存、in-flight 请求合并、对象池索引
  • 关键点:优先使用 computeIfAbsent 等原子 API,避免重复创建与竞态
  • 详见 map-concurrency-compare

3. 状态位与生命周期控制

  • AtomicBoolean / volatile 管理一次性启动、停止信号、熔断开关
  • 关键点:状态位可见,不代表整个对象状态机天然安全

4. 高并发计数与统计

  • QPS、命中数、失败数、限流窗口统计
  • 低到中等冲突:AtomicLong
  • 热点极高:考虑 LongAdder

5. 生产者-消费者

  • 任务队列、事件分发、日志异步写盘
  • 关键点:背压、队列上限、消费失败策略,不只是“丢给线程池”

常见坑

  1. 直接 new Thread 处理业务任务

    • 结果:线程数量不可控、缺乏统一生命周期治理、排障困难。
  2. 在线程池内部相互等待 Future.get()

    • 尤其固定大小线程池中,极易造成线程池饥饿甚至死锁。
  3. volatile 当线程安全万能钥匙

    • 它只能解决可见性与部分有序性,解决不了复合操作原子性。
  4. ConcurrentHashMap 包住不安全复合逻辑

    • 容器线程安全 ≠ 基于容器的业务流程线程安全。
  5. 锁范围过大

    • 把 IO、RPC、数据库访问放进锁里,会把临界区从纳秒级放大到毫秒甚至秒级。
  6. 锁顺序不固定

    • 多资源场景中没有统一获取顺序,是死锁高发源头。
  7. 忽视中断语义

    • 取消任务只调 cancel(true),但任务内部从不检查中断或调用可中断阻塞 API,取消就形同虚设。
  8. 默认线程池参数直接上线

    • 不分析任务性质、不限队列长度、不配置命名和监控,最终问题都在线上暴露。
  9. 安全发布缺失

    • 单例、配置对象、缓存值初始化后直接跨线程读,偶发读到半初始化状态。

与相近概念对比

Thread vs Runnable vs Callable vs Future

概念它是什么解决什么问题不解决什么
Thread执行载体真正跑起来任务复用、结果治理、资源控制
Runnable无返回值任务描述要执行的动作结果返回、异常建模
Callable有返回值任务描述一次异步计算调度策略
Future异步结果句柄查询/等待/取消结果线程创建、任务调度

synchronized vs Lock vs volatile

概念主要能力适合场景典型误区
synchronized互斥 + 可见性临界区明确、简单互斥以为性能一定差、以为过时
Lock互斥 + 更强控制可中断、超时、公平、多个条件队列为了“高级感”滥用
volatile可见性 + 有序性约束状态位、引用发布当成轻量锁使用

Atomic* vs Lock

维度Atomic*Lock
机制CAS 自旋重试阻塞/唤醒 + 临界区互斥
优势低冲突下轻量、单变量表达直接能保护复杂复合逻辑
劣势高冲突烧 CPU、表达复杂事务差切换和等待成本更高
适用计数、标记位、引用更新多字段一致性、长临界区、复杂约束

ConcurrentHashMap vs synchronizedMap

维度ConcurrentHashMapCollections.synchronizedMap
并发粒度更细整体串行化倾向更强
复合原子操作提供 putIfAbsent / compute*需外部再加锁
高并发吞吐更适合容易形成全局锁瓶颈

对应实验

各章节内已嵌入跳转链接;此处汇总全部 Lab:

Lab说明源码
thread-pool-basicsthread-pool-basicsThreadPoolBasics.java
queue-strategy-comparequeue-strategy-compareQueueStrategyCompare.java
volatile-vs-atomic-vs-lockvolatile-vs-atomic-vs-lockVolatileVsAtomicVsLock.java
singleton-dclsingleton-dclSingletonDcl.java
map-concurrency-comparemap-concurrency-compareMapConcurrencyCompare.java

建议补齐的实验说明如下:

  1. thread-pool-basics

    • 演示 Thread / Runnable / Callable / Future 的差异
    • 重点观察:Future.get() 阻塞、异常传播、取消行为
  2. queue-strategy-compare

    • 用不同 core/max/queue/reject 组合压测线程池
    • 重点观察:排队、扩线程、拒绝策略对吞吐和尾延迟的影响
  3. volatile-vs-atomic-vs-lock

    • 同一个计数器分别用 volatileAtomicIntegersynchronized
    • 重点观察:错误结果、正确性、吞吐差异
  4. singleton-dcl

    • 对比错误单例、方法同步单例、DCL、静态内部类、enum 单例
    • 重点观察:安全发布、懒加载、实现复杂度与工程推荐度边界
  5. map-concurrency-compare(已建立)

    • 对比 HashMap / synchronizedMap / ConcurrentHashMap
    • 重点观察:get-then-putcomputeIfAbsent、重复初始化
  6. deadlock-and-thread-dump

    • 构造双锁死锁并抓线程 dump
    • 重点观察:BLOCKED 状态、锁拥有者链路
  7. deadlock-and-thread-dump

    • 构造双锁死锁并抓线程 dump
    • 重点观察:BLOCKED 状态、锁拥有者链路
  8. jcstress-safe-publication

    • 验证对象未安全发布时的异常可见结果
    • 重点观察:JMM 问题为何不能靠肉眼推理

复习检查题

  1. 为什么说 Future 是结果句柄,不是并发模型本身?

    Future 只负责拿结果、取消、查状态get/cancel/isDone),不负责调度与执行线程;真正跑任务的是 ExecutorService、线程或框架调度器。把它当「并发模型」会误把票据当成发动机。

  2. 固定线程池里任务 A 等待任务 B 的 Future.get(),在什么条件下会导致线程池饥饿或死锁?

    :当池大小有限,且池内任务互相 get() 等待彼此时:所有工作线程都在阻塞等 Future,队列里的任务得不到线程执行,形成池内死锁/饥饿。典型于固定小池 + 任务内同步等待同池提交的子任务。修法:扩大池、子任务用独立池、或避免在池内阻塞 get()

  3. volatile 能保证什么,不能保证什么?请用 count++ 解释。

    :保证可见性禁止特定重排序,建立有限 happens-before。不保证复合操作原子性。count++ = 读、加、写三步,多线程仍可能基于同一旧值各自加一,最终少计。计数应改用 AtomicInteger 或锁。

  4. 为什么 ConcurrentHashMap 不能自动让 if (get == null) put() 变成线程安全?

    :CHM 保证的是单次 API 调用原子,而 getput 是两次独立操作,中间可被别的线程插入。两线程都可能看到 null 后各 put 一次,产生重复初始化或覆盖。应使用 putIfAbsent / computeIfAbsent原子复合 API

  5. synchronizedReentrantLock 的选择边界是什么?什么情况下 Lock 的额外复杂度是值得的?

    :默认 synchronized:临界区短、语义简单、JVM 优化成熟。ReentrantLock 在需要 tryLock、可中断等待、公平策略、Condition 多条件队列时值得引入。代价是必须 try/finally 释放,控制流更复杂;不是「更高级就该用」。

  6. JMM 里的可见性、原子性、有序性分别解决什么问题?它们为什么不是一回事?

    可见性:别的线程何时能看见我的写。原子性:操作会不会被看到中间态(如 count++ 三步)。有序性:代码书写顺序与多线程观察顺序是否一致(重排序)。三者独立:例如 volatile 改善可见性/有序性,但不给 count++ 原子性。

  7. 什么叫安全发布?为什么「双重检查锁 + volatile」少了 volatile 就不成立?

    安全发布指其他线程看到的引用指向已完全构造好的对象。new 可能被重排为:先赋引用、再执行构造。无 volatile 时,另一线程可能在第二步就看到非空引用,读到半初始化对象volatile 禁止这种有害重排,使发布与初始化顺序对读者可见。

  8. 死锁、活锁、竞态条件在线上症状上通常各长什么样?

    死锁:线程长期 BLOCKED/WAITING,吞吐归零,dump 见环路持锁。活锁:CPU 高、线程在跑,但业务无进展(如 CAS 疯狂重试、双方礼让循环)。竞态:偶现、难复现的数据错、重复初始化、取消后仍回调,依赖时序碰运气。

  9. Android 中哪些问题表面像 UI 卡顿,根因其实是后台线程争锁或线程池治理失衡?

    :主线程 Future.get()/同步锁等待后台;图片/网络池过小导致任务堆积后反压到主线程(如 CallerRunsPolicy 在主线程跑重任务);Binder/共享缓存争锁传导到 UI 线程;Handler 队列被阻塞任务拖慢。ANR trace 常显示主线程在等锁或 get(),而非布局本身慢。

  10. 对一个高并发共享计数场景,你会如何在 AtomicLongLongAdder、锁之间做取舍?

    低冲突、逻辑简单AtomicLong极高并发写、可接受读时求和LongAdder计数与多字段状态联动、需事务性:锁或不可变状态整体替换。避免在热点上用粗粒度锁;也避免在复杂一致性上硬凑 CAS。

速记

  • Thread 是载体,Runnable/Callable 是任务,Future 是票据,ExecutorService 是治理层。
  • 共享内存并发的核心不是“能不能异步”,而是“共享状态如何安全”。
  • synchronized 先解决简单互斥,Lock 只在需要更强控制时引入。
  • volatile 只保可见性和部分有序性,不保复合原子性。
  • Atomic* 适合单变量原子更新;复杂一致性问题别硬上 CAS。
  • ConcurrentHashMap 保证容器级并发安全,不替你保证业务复合逻辑安全。
  • JMM 记一句:没有 happens-before,就不要脑补另一个线程一定能看见。
  • 线上排查先分型:卡死看锁和线程 dump,CPU 高看自旋/活锁,脏数据看竞态和发布。
  • 线程池参数就是系统行为参数;默认值不是架构设计。