JAVA CONCURRENT · JUC / 线程安全

Java 并发速查

从线程生命周期、线程池七参数,到锁升级、JMM 可见性规则、并发容器与 AQS 原理,JUC 面试高频与业务并发控制一张表收齐,随查随用。

8速查小节 94速查条目 7大线程池参数 6种线程状态

📖 速查表

点击展开各小节

🧵 线程基础
创建方式说明示例
继承 Thread 重写 run();Java 单继承,无法再继承其他类 new Thread() { public void run() {...} }.start();
实现 Runnable 无返回值、不能抛受检异常;任务与线程解耦,推荐配合线程池 new Thread(() -> task()).start();
Callable + FutureTask 有返回值、可抛异常;get() 阻塞获取结果 new FutureTask<>(() -> 42); new Thread(ft).start(); ft.get();
线程池托管 复用线程、控制并发数,生产环境统一入口 推荐 executor.submit(() -> task())
状态(6 种)含义进入 / 退出
NEW 已创建尚未启动 new Thread() 之后、start() 之前
RUNNABLE 可运行(含就绪与运行中,映射 OS 的就绪 / 运行态) start() 后,等待或持有 CPU 时间片
BLOCKED 等待 monitor 锁 进入 synchronized 未抢到锁;抢到后回到 RUNNABLE
WAITING 无限期等待,需被显式唤醒 wait() / join() / LockSupport.park()
TIMED_WAITING 限时等待,超时自动返回 sleep(ms) / wait(ms) / join(ms) / parkNanos()
TERMINATED 终止态,生命周期结束 run() 执行完成或抛出未捕获异常
方法所属关键区别
sleep Thread 静态方法 只让出 CPU 不释放锁,到时自动恢复;任意位置可调用,不响应 notify
wait Object 实例方法 必须在 synchronized 内调用;释放对象锁进入等待队列,由 notify / notifyAll 唤醒
join Thread 实例方法 底层即 wait(0);当前线程等目标线程执行结束再继续(主线程等子线程)
yield Thread 静态方法 仅提示调度器让出时间片的 hint,可能立刻被再次调度;不释放锁
🏊 线程池
核心参数说明要点
corePoolSize 核心线程数,默认常驻不回收 allowCoreThreadTimeOut(true) 可让核心线程也超时回收
maximumPoolSize 最大线程数(核心 + 非核心上限) 队列满之后才会创建非核心线程
keepAliveTime 非核心线程空闲存活时间 超过后空闲线程被回收,抑制线程膨胀
unit keepAliveTime 的时间单位 TimeUnit.SECONDS
workQueue 工作队列,缓存待执行任务 有界 ArrayBlockingQueue / 无界 LinkedBlockingQueue / SynchronousQueue
threadFactory 线程工厂,定制创建过程 自定义线程名便于排查问题,还可设置优先级、异常处理器
handler 拒绝策略,队列与线程全满时触发 默认 AbortPolicy,按业务选择或自定义
步骤判定条件行为
1. 核心线程 运行线程数 < corePoolSize 直接创建核心线程执行(即使已有空闲线程)
2. 入队 达到核心线程数 任务进入 workQueue 排队等待
3. 非核心线程 队列已满且线程数 < maximumPoolSize 创建非核心线程执行队头任务
4. 拒绝 达到最大线程数且队列已满 触发拒绝策略 handler 过载
拒绝策略行为适用场景
AbortPolicy 直接抛出 RejectedExecutionException 默认策略,需要感知过载并及时告警的场景 默认
CallerRunsPolicy 由提交任务的线程自己执行该任务 天然反压:提交线程被拖慢,生产者自动减速 不丢任务
DiscardPolicy 静默丢弃新提交的任务 可容忍丢失的任务,如日志埋点、监控上报
DiscardOldestPolicy 丢弃队首最老任务,腾位后重试提交 只关心最新数据的场景,如行情报价、实时位置
经验值 / 陷阱建议
CPU 密集型 核心线程数 = N(CPU 核数)或 N + 1,减少上下文切换开销
IO 密集型 核心线程数 ≈ 2N,或按 N * (1 + 等待时间 / 计算时间) 估算,最终以压测定参
newFixedThreadPool / newSingleThreadExecutor 使用无界 LinkedBlockingQueue,任务持续堆积导致 OOM 陷阱
newCachedThreadPool / newScheduledThreadPool maximumPoolSize 为 Integer.MAX_VALUE,线程无限创建导致 OOM 陷阱
为什么不用 Executors 手动 new ThreadPoolExecutor(...),显式指定有界队列、线程名与拒绝策略,资源可控
线程池工作流
提交顺序:核心线程 → 阻塞队列 → 非核心线程 → 拒绝策略
🔒 锁与同步
synchronized 用法锁对象说明
修饰实例方法 当前对象 this 同一对象的该方法调用互斥,不同对象互不影响
修饰静态方法 Class 对象 全局唯一,类的所有实例之间都互斥
修饰代码块 指定对象 synchronized(obj) 粒度最细,推荐尽量缩小临界区减少竞争
锁升级(1.6+,单向不可逆)实现说明
无锁 对象头 Mark Word 初始状态 尚未被任何线程竞争访问
偏向锁 Mark Word 记录线程 ID,同线程重入免 CAS 只有一个线程访问时最优;撤销需等安全点,高竞争下反而拖慢(JDK 15 起默认废弃,JEP 374)
轻量级锁 栈帧 Lock Record + CAS 自旋抢锁 少量线程交替、持锁时间短时,避免 OS 互斥量开销
重量级锁 膨胀为 monitor(OS 互斥量) 未抢到的线程挂起阻塞,涉及用户态 / 内核态切换,开销最大
维度ReentrantLocksynchronized
实现层面 JDK API 层,基于 AQS JVM 层 monitorenter / monitorexit 字节码指令
锁释放 手动 unlock(),必须放 finally 代码块结束或异常自动释放
公平性 构造可选公平 / 非公平锁 只有非公平锁
高级功能 可中断 lockInterruptibly()、超时 tryLock()、多 Condition 条件队列 不支持中断、超时与多条件队列
性能 1.6 锁优化后两者基本持平 简单场景优先 synchronized,复杂协调再上 Lock
读写锁说明注意
ReentrantReadWriteLock 读读共享、读写 / 写写互斥;state 高 16 位记读、低 16 位记写 适合读多写少;读持续占用时写线程可能饥饿
StampedLock JDK 8+:写锁 + 悲观读 + 乐观读(不加阻塞,读后 validate(stamp) 校验版本) 不可重入、不支持 Condition;乐观读失败需升级悲观读重试
⚛️ JMM 与关键字
happens-before 规则内容
程序顺序规则 单线程内,前面的操作 happens-before 后面的操作(保证看起来有序)
监视器锁规则 对同一把锁,解锁 happens-before 后续的加锁
volatile 规则 对 volatile 变量的写 happens-before 任意线程后续对它的读
线程启动规则 Thread.start() 之前的操作,对新线程内任意操作可见
线程终止规则 线程内所有操作,happens-before 其他线程检测到它结束(join() 返回 / isAlive() 为 false)
中断规则 对线程 interrupt() 的调用,happens-before 被中断线程检测到中断
传递性 A HB B 且 B HB C,则 A HB C;规则可串联推理可见性
volatile说明
可见性 写立即刷回主存并使其他 CPU 缓存失效,读总是加载最新值 作用一
有序性 内存屏障禁止指令重排;典型:DCL 单例中 new 的分配 / 初始化 / 赋值不重排 作用二
限制 不保证原子性:i++ 是读改写三步仍会丢更新,需 CAS 或锁 不能替代锁
ThreadLocal说明
原理 每个 Thread 内持有 ThreadLocalMap,以 ThreadLocal 弱引用为 key、线程私有副本为 value,空间换隔离无竞争
内存泄漏点 key 是弱引用被 GC 后 Entry 变 (null, value),value 强引用仍无法回收;线程池线程长存活时持续累积
最佳实践 用完必须 remove()(放 finally 中);线程池里跨线程传递上下文用 TransmittableThreadLocal,解决线程复用导致的上下文错乱
CAS 与 ABA说明
CAS compareAndSwap 依赖 CPU 原子指令(lock cmpxchg)实现乐观并发,失败自旋重试不阻塞;JUC 的底层基座
ABA 问题 值 A→B→A 时 CAS 感知不到中间变化;用 AtomicStampedReference 加版本戳(或 MarkableReference 布尔标记)解决
📦 并发容器与协作工具
维度ConcurrentHashMap 1.7ConcurrentHashMap 1.8
数据结构 Segment 分段锁数组 + HashEntry 数组 Node 数组 + 链表 + 红黑树(与 HashMap 对齐)
锁粒度 锁一个 Segment(默认 16 段,并发度固定) 锁单个桶头节点:CAS 空桶 + synchronized 非空桶
size 统计 先无锁多次尝试统计,失败再锁全表统计 baseCount + CounterCell[] 分段计数(LongAdder 思路)
扩容 Segment 内部扩容,段间独立 支持多线程协助迁移(transfer,ForwardingNode 标记)
CopyOnWriteArrayList说明
写时复制机制 写操作加锁复制新数组、修改后替换引用;读完全无锁走旧数组,读写不互斥
适用场景 读极多写极少:监听器列表、黑白名单、配置快照;写有复制开销且迭代器为弱一致性快照
协作工具核心方法适用场景
CountDownLatch countDown() / await(),一次性倒数不可重置 主线程等 N 个子任务全部完成后汇总
CyclicBarrier await(),计数归零自动重置可复用,支持屏障回调 N 个线程互相等齐再一起走,如分阶段并行计算
Semaphore acquire() / release(),维护许可数 限流与并发资源控制,如数据库连接池配额
CompletableFuture API说明示例
supplyAsync / runAsync 提交异步任务(有 / 无返回值);默认 ForkJoinPool.commonPool,生产建议传自定义线程池 CompletableFuture.supplyAsync(() -> query(), bizPool)
thenApply / thenAccept / thenRun 串链处理上一步结果:转换 / 消费 / 纯执行 cf.thenApply(r -> r * 2)
thenCombine / thenCompose 合并两个独立结果 / 串联依赖的异步任务(flatMap 语义) f1.thenCombine(f2, (a, b) -> a + b)
allOf / anyOf 等待全部 / 任一任务完成 CompletableFuture.allOf(f1, f2).join()
exceptionally / handle / whenComplete 异常兜底:只处理异常 / 异常与结果都可转换并返回 / 仅做收尾不改变结果 cf.exceptionally(e -> defaultVal)
get vs join 都阻塞取结果;get() 抛受检异常需 try-catch,join() 抛非受检异常写法更简洁 流式链路中常用 join()
🏗️ AQS 与原子类
AQS 核心概念说明
state 共享变量 volatile int state + CAS 修改,抽象同步资源:ReentrantLock 的重入次数、Semaphore 的许可数
CLH 变体等待队列 抢锁失败的线程封装为 Node 入双向队列阻塞挂起;释放时头节点唤醒后继节点
独占 / 共享模式 独占一次只有一个线程持有(ReentrantLock);共享可多个同时获取(Semaphore、CountDownLatch、读锁)
模板方法设计 AQS 负责排队、阻塞、唤醒;子类只需重写 tryAcquire / tryRelease / tryAcquireShared 等钩子
常用原子类说明
AtomicInteger / AtomicLong CAS 原子自增自减,计数器、序列号场景
AtomicReference / FieldUpdater 对象引用原子更新;FieldUpdater 以反射方式给普通字段补原子能力,省包装对象
AtomicStampedReference 引用 + 版本戳一起 CAS,解决 ABA 问题(MarkableReference 用布尔标记位)
LongAdder / LongAccumulator 分段 Cell 累加,高并发写场景性能优先;读为近似汇总值
维度AtomicLongLongAdder
原理 单个 volatile long + CAS 自旋 base + Cell[] 分段:无竞争写 base,有竞争散列到各 Cell
写性能 高竞争下 CAS 失败自旋,吞吐明显下降 热点分散,写吞吐随核数近线性提升
读精度 get() 实时精确值 sum() 汇总各 Cell,返回非精确的瞬时值
🧨 死锁现场与排查

死锁四条件:互斥、持有并等待、不可剥夺、循环等待——破坏任意一个即可预防。线上最常见的是「两把锁交叉获取」,下面是完整现场与 jstack 排查读法。

// ❌ 死锁现场:两把锁交叉获取(加锁顺序不一致 = 根因) Object lockA = new Object(), lockB = new Object(); // 线程 1:先拿 A,再拿 B new Thread(() -> { synchronized (lockA) { // T1 已持有 lockA try { Thread.sleep(100); } catch (InterruptedException ignored) {} // 制造交叉窗口,让 T2 先拿到 B synchronized (lockB) { ... } // 等 lockB —— 被 T2 持有,BLOCKED } }).start(); // 线程 2:先拿 B,再拿 A(顺序与 T1 相反) new Thread(() -> { synchronized (lockB) { // T2 已持有 lockB synchronized (lockA) { ... } // 等 lockA —— 与 T1 互相等待 } }).start(); // ---- 排查:jstack <pid> 直接给出结论,重点读三处 ---- // ① 每个线程的 "waiting to lock <0x...>"(想要的锁) // 与 "locked <0x...>"(已持有的锁),地址对号入座: "Thread-0": - locked <0x000000076ab621d8> (a java.lang.Object) - waiting to lock <0x000000076ab62208> (a java.lang.Object) "Thread-1": - locked <0x000000076ab62208> (a java.lang.Object) - waiting to lock <0x000000076ab621d8> (a java.lang.Object) // ② 两个线程的锁地址互相指向对方 = 循环等待实锤 // ③ 翻到输出末尾,JVM 已自动检测并汇总: Found one Java-level deadlock: ============================= "Thread-1": waiting to lock <0x000000076ab621d8> (a java.lang.Object), which is held by "Thread-0"(结论:互相持有对方要的锁,按提示修复加锁顺序) // 修复:全局统一加锁顺序(先 A 后 B),或用 tryLock(timeout) 替代无限等待
⚡ 虚拟线程速览

JDK 21 正式可用的轻量线程(Project Loom):由 JVM 调度到少量载体线程上运行,阻塞时自动让出载体,百万级并发不再是奢侈。但它不是万能替代品。

维度说明要点
适用场景 IO 密集 + 高并发:RPC 调用、DB 查询、HTTP 网关类请求,阻塞频繁但每次耗时短 任务量越大、阻塞越多,相对平台线程的收益越明显 首选
不适用场景 CPU 密集计算(纯占用 CPU 无阻塞,虚拟化无收益);pinning 场景:synchronized 块内阻塞或调用 native 方法时钉住载体线程 热点 synchronized 改 ReentrantLock 规避 pinning(JDK 24 已大幅缓解)
与线程池的关系 不再池化:虚拟线程创建成本极低,用完即弃,一个任务一个线程 不要池化、不要复用;ThreadLocal 大对象会随海量线程放大内存,改用 ScopedValue
JDK 21+ 用法 Thread.ofVirtual().name("v-", 0).start(task);Spring 场景用 Executors.newVirtualThreadPerTaskExecutor() Boot 3.2 起 spring.threads.virtual.enabled=true 一键切换 Tomcat 虚拟线程