Java 并发编程实战指南:JMM、synchronized、AQS、线程池与并发容器
Java 并发是大厂面试的重灾区,也是线上事故的高发区:可见性问题导致标志位不生效、死锁拖垮整个服务、线程池参数配错引发 OOM、ConcurrentHashMap 用错顺序导致重复初始化。本文按"内存模型 → 关键字 → 锁 → AQS → 线程池 → 并发容器 → 工具类 → 实战"的主线讲透,每节配可运行代码和真实坑。
一、JMM(Java 内存模型)
1.1 为什么需要 JMM
硬件现实:
- CPU 有缓存(L1/L2/L3),读写不直接到主存
- 编译器/CPU 会重排序(提高性能)
- 多核各自缓存 → 数据不一致
JMM 要解决:
1. 可见性:一个线程的修改另一个线程何时能看到
2. 有序性:指令重排的边界
3. 原子性:操作不可分割
1.2 主内存与工作内存
JMM 抽象:
- 主内存(所有线程共享)
- 工作内存(每个线程私有,存变量副本)
线程读写流程:
read(从主存读)→ load(载入工作内存)
→ use(使用)
→ assign(赋值)
→ store(写回工作内存)→ write(写回主存)
1.3 三大特性
原子性:i++ 不是原子操作(读-改-写三步)
可见性:A 改了 flag,B 可能看不到(缓存)
有序性:指令重排(编译期/CPU 期)
1.4 happens-before 规则(核心)
1. 程序顺序:同一线程内,前面的操作 hb 后面的
2. 锁规则:unlock hb 后续 lock
3. volatile:写 hb 后续读
4. 线程启动:start() hb 线程内所有操作
5. 线程终止:线程内所有操作 hb join() 返回
6. 传递性:A hb B,B hb C → A hb C
作用:只要满足 hb,前一个操作的结果对后一个可见(且不被重排越过)
二、volatile
2.1 两个语义
1. 可见性:写 volatile → 立即刷主存;读 → 从主存读
2. 有序性:禁止重排序(内存屏障)
2.2 内存屏障
写前:StoreStore(前面普通写先完成)
写后:StoreLoad(写后插入,禁止后面的读写越过)
读前:LoadLoad
读后:LoadStore
2.3 经典用法:状态标志
public class Worker {
private volatile boolean running = true;
public void stop() {
running = false; // 写 volatile
}
public void run() {
while (running) { // 读 volatile(每次从主存读)
doWork();
}
}
}
不加 volatile:
JIT 可能把 running 提到寄存器 → 循环永远读旧值 → 停不下来
2.4 volatile 不能做什么
❌ 不能保证原子性
private volatile int count;
count++; // 读-改-写三步,仍会丢更新
✅ 正确:AtomicInteger / synchronized / LongAdder
2.5 DCL 单例(volatile 的经典场景)
public class Singleton {
private static volatile Singleton instance;
public static Singleton getInstance() {
if (instance == null) { // 第一次检查
synchronized (Singleton.class) {
if (instance == null) { // 第二次检查
instance = new Singleton(); // 分三步:分配/初始化/赋值
}
}
}
return instance;
}
}
不加 volatile 的问题:
new 的三步可能被重排为 分配→赋值→初始化
→ 另一个线程看到非 null 但未初始化完的对象 → 半成品
三、synchronized
3.1 三种用法
// 1. 实例方法(锁 this)
public synchronized void m1() {}
// 2. 静态方法(锁 Class 对象)
public static synchronized void m2() {}
// 3. 代码块(锁指定对象)
public void m3() {
synchronized (lock) { }
}
3.2 锁升级(对象头 Mark Word)
无锁 → 偏向锁 → 轻量级锁(自旋)→ 重量级锁(OS 互斥量)
偏向锁:同一线程反复进入,记录线程 ID(无 CAS)
轻量级:多线程交替,CAS 自旋
重量级:竞争激烈,阻塞挂起(内核态)
JDK 15 起偏向锁默认关闭(收益低、撤销成本高)
3.3 底层原理
monitorenter / monitorexit 字节码指令
每个对象关联一个 Monitor(管程):
- owner(持有线程)
- entryList(阻塞队列)
- waitSet(wait 等待集)
3.4 实战:锁的粒度
// ❌ 锁整个方法(粒度太大,串行化)
public synchronized void update() {
validate(); // 不需要锁
doUpdate(); // 需要锁
sendNotify(); // 不需要锁
}
// ✅ 只锁关键区
public void update() {
validate();
synchronized (this) { doUpdate(); }
sendNotify();
}
四、AQS(AbstractQueuedSynchronizer)
4.1 核心结构
public abstract class AbstractQueuedSynchronizer {
private volatile int state; // 同步状态
private transient volatile Node head; // 等待队列头
private transient volatile Node tail; // 等待队列尾
// 模板方法(子类实现)
protected boolean tryAcquire(int arg);
protected boolean tryRelease(int arg);
protected int tryAcquireShared(int arg);
protected boolean tryReleaseShared(int arg);
}
state 语义由子类定义:
- ReentrantLock:0 未锁,>0 重入次数
- Semaphore:剩余许可数
- CountDownLatch:剩余计数
4.2 独占模式(ReentrantLock)
// 加锁流程(简化)
final void lock() {
if (compareAndSetState(0, 1)) { // 快速路径:CAS 抢锁
setExclusiveOwnerThread(current);
} else {
acquire(1); // 慢路径:入队等待
}
}
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) {
selfInterrupt();
}
}
流程:
1. tryAcquire 再试一次(可能刚好释放)
2. 失败 → 包装成 Node 入队(CAS 到尾)
3. 自旋检查前驱是否是 head + 能否抢到
4. 不能 → park 阻塞(LockSupport.park)
5. 被唤醒 → 重试
4.3 共享模式(Semaphore/CountDownLatch)
// CountDownLatch
protected int tryAcquireShared(int acquires) {
return (getState() == 0) ? 1 : -1; // 计数为 0 才成功
}
protected boolean tryReleaseShared(int releases) {
for (;;) {
int c = getState();
if (c == 0) return false;
int next = c - 1;
if (compareAndSetState(c, next)) {
return next == 0; // 减到 0 → 唤醒所有
}
}
}
4.4 手写一个不可重入锁(理解 AQS)
public class SimpleLock implements Lock {
private final Sync sync = new Sync();
static class Sync extends AbstractQueuedSynchronizer {
@Override
protected boolean tryAcquire(int arg) {
if (compareAndSetState(0, 1)) {
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
return false;
}
@Override
protected boolean tryRelease(int arg) {
if (getState() == 0) throw new IllegalMonitorStateException();
setExclusiveOwnerThread(null);
setState(0);
return true;
}
@Override
protected boolean isHeldExclusively() {
return getState() == 1;
}
}
public void lock() { sync.acquire(1); }
public void unlock() { sync.release(1); }
// ...
}
五、ReentrantLock 与 synchronized 对比
| 维度 | synchronized | ReentrantLock |
|---|---|---|
| 实现 | JVM 内置(monitor) | JDK 层(AQS) |
| 释放 | 自动(异常也释放) | 必须 finally unlock |
| 可中断 | 不支持 | lockInterruptibly |
| 超时 | 不支持 | tryLock(timeout) |
| 公平锁 | 非公平 | 可选公平/非公平 |
| 条件变量 | wait/notify(一个) | 多 Condition |
| 性能 | 优化后接近 | 相近 |
// ReentrantLock 标准写法
private final ReentrantLock lock = new ReentrantLock();
public void doSomething() {
lock.lock();
try {
// 临界区
} finally {
lock.unlock(); // 必须 finally
}
}
选择:
- 简单同步 → synchronized(不易错)
- 需要超时/中断/公平/多条件 → ReentrantLock
六、线程池(ThreadPoolExecutor)
6.1 七个参数
new ThreadPoolExecutor(
corePoolSize, // 核心线程数(常驻)
maximumPoolSize, // 最大线程数
keepAliveTime, // 非核心线程空闲存活
unit, // 时间单位
workQueue, // 任务队列
threadFactory, // 线程工厂(命名很重要)
handler // 拒绝策略
);
6.2 执行流程(必背)
1. 线程数 < core → 新建核心线程执行
2. 线程数 >= core → 任务入队列
3. 队列满 → 新建非核心线程(直到 max)
4. 达到 max 且队列满 → 拒绝策略
注意:不是"先扩容再排队",是"先排队再扩容"
6.3 四种拒绝策略
AbortPolicy 抛 RejectedExecutionException(默认)
CallerRunsPolicy 调用线程自己执行(降速,不丢任务)
DiscardPolicy 静默丢弃
DiscardOldestPolicy 丢最老的,再提交
6.4 队列选择
ArrayBlockingQueue 有界(推荐,防 OOM)
LinkedBlockingQueue 无界(默认 Integer.MAX_VALUE)→ 危险!
SynchronousQueue 不存(直接交给线程)
PriorityBlockingQueue 优先级
坑:Executors.newFixedThreadPool 用无界队列
→ 任务堆积 → 内存暴涨 → OOM
→ 阿里规约禁止用 Executors 创建
6.5 参数怎么定
CPU 密集型:N(核数)+ 1
IO 密集型:2N 或 N * (1 + 等待时间/计算时间)
实际:先压测,再根据 QPS/耗时调整
队列容量:要有界(如 1000-5000),配合拒绝策略
6.6 标准配置
ThreadPoolExecutor pool = new ThreadPoolExecutor(
8, 32,
60L, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(2000),
new ThreadFactoryBuilder().setNameFormat("order-pool-%d").build(),
new ThreadPoolExecutor.CallerRunsPolicy()
);
要点:
- 线程命名(排查时能定位)
- 有界队列
- 拒绝策略明确
- 监控(活跃线程/队列长度/完成任务数)
6.7 监控
// 定期打印
log.info("pool size={} active={} queue={} completed={}",
pool.getPoolSize(), pool.getActiveCount(),
pool.getQueue().size(), pool.getCompletedTaskCount());
七、并发容器
7.1 ConcurrentHashMap
JDK 8 结构:
- 数组 + 链表 + 红黑树
- CAS + synchronized 锁单个桶(不是分段锁)
- size 用 CounterCell 分散计数(类似 LongAdder)
// 用法要点
ConcurrentHashMap<String, List<String>> map = new ConcurrentHashMap<>();
// ❌ 非原子(先 get 再 put 有竞态)
List<String> list = map.get(key);
if (list == null) {
list = new ArrayList<>();
map.put(key, list);
}
list.add(value);
// ✅ 原子操作
map.computeIfAbsent(key, k -> new ArrayList<>()).add(value);
注意:
- 不允许 null key/value(无法区分"不存在")
- 复合操作必须用 compute/putIfAbsent/merge
7.2 CopyOnWriteArrayList
写时复制:写操作复制整个数组(加锁),读无锁
适用:读多写极少(配置/监听器列表)
不适用:写频繁(复制成本高、内存翻倍)
List<String> listeners = new CopyOnWriteArrayList<>();
listeners.add(listener); // 写:复制
for (String l : listeners) {} // 读:快照,无锁
7.3 阻塞队列
ArrayBlockingQueue 数组,有界,单锁
LinkedBlockingQueue 链表,可有界,双锁(吞吐高)
PriorityBlockingQueue 优先级,无界
DelayQueue 延迟队列(定时任务)
SynchronousQueue 不存储(直接移交)
// 生产者-消费者
BlockingQueue<Task> queue = new ArrayBlockingQueue<>(1000);
// 生产
queue.put(task); // 满了阻塞
// 或 queue.offer(task, 1, TimeUnit.SECONDS);
// 消费
Task t = queue.take(); // 空了阻塞
八、并发工具类
8.1 CountDownLatch(等N个完成)
CountDownLatch latch = new CountDownLatch(3);
for (int i = 0; i < 3; i++) {
new Thread(() -> {
try { doWork(); }
finally { latch.countDown(); }
}).start();
}
latch.await(); // 主线程等 3 个都完成
// 或 latch.await(5, TimeUnit.SECONDS); 超时
一次性,计数到 0 不能重置
8.2 CyclicBarrier(互相等待再一起走)
CyclicBarrier barrier = new CyclicBarrier(3, () -> {
System.out.println("全部到齐,开始下一阶段");
});
for (int i = 0; i < 3; i++) {
new Thread(() -> {
for (int round = 0; round < 2; round++) {
doPhase(round);
barrier.await(); // 等所有线程到齐
}
}).start();
}
可循环使用(reset)
区别:CountDownLatch 是"一个等多个";CyclicBarrier 是"多个互等"
8.3 Semaphore(限流)
Semaphore semaphore = new Semaphore(10); // 最多 10 个并发
public void handle() {
semaphore.acquire();
try {
doHandle();
} finally {
semaphore.release();
}
}
// 公平模式(按等待顺序)
new Semaphore(10, true);
8.4 原子类与 LongAdder
AtomicInteger count = new AtomicInteger(0);
count.incrementAndGet(); // CAS 自旋
// 高并发计数:LongAdder(分散热点,分段累加)
LongAdder adder = new LongAdder();
adder.increment();
long total = adder.sum();
选择:
- 低竞争、需要精确值 → AtomicLong
- 高并发累加(统计)→ LongAdder
九、常见坑与排查
9.1 死锁
// 典型:两把锁交叉获取
Thread A: lock1.lock() → lock2.lock()
Thread B: lock2.lock() → lock1.lock()
排查:
jstack <pid> | grep -A 20 "Found one Java-level deadlock"
预防:
- 固定加锁顺序
- 用 tryLock(timeout)
- 减少锁嵌套
9.2 线程池 OOM
原因:无界队列 + 任务无限堆积
排查:看堆 dump(大量 Task 对象)
解决:有界队列 + 拒绝策略 + 限流
9.3 ThreadLocal 泄漏
// ❌ 线程池复用线程,ThreadLocal 不清理
private static final ThreadLocal<User> USER = new ThreadLocal<>();
USER.set(user);
// 用完不 remove → 线程复用仍持有 user → 内存泄漏
// ✅ finally 清理
try {
USER.set(user);
doWork();
} finally {
USER.remove();
}
9.4 伪共享
多个变量在同一缓存行(64 字节)→ 一个线程改,另一个线程缓存失效
解决:@Contended(需 -XX:-RestrictContended)或手动填充
(Disruptor/LongAdder 内部用了)
9.5 线程数过多
每线程栈 1MB(默认)
1000 线程 = 1GB 栈内存
→ 用线程池,别 new Thread
十、常见坑速查
| 现象 | 原因 | 解决 |
|---|---|---|
| 标志位不生效 | 无 volatile | volatile |
| i++ 丢更新 | 非原子 | Atomic/synchronized |
| 单例半成品 | DCL 无 volatile | volatile |
| 死锁 | 交叉加锁 | 固定顺序/tryLock |
| 池 OOM | 无界队列 | 有界 + 拒绝策略 |
| ThreadLocal 泄漏 | 未 remove | finally remove |
| CHM 数据错 | 复合操作非原子 | computeIfAbsent |
| 计数慢 | 热点 CAS | LongAdder |
| 任务丢 | DiscardPolicy | CallerRuns/自定义 |
本章小结
Java 并发的主线:JMM(可见性/有序性/原子性 + happens-before)→ volatile(可见性 + 屏障,不保证原子)→ synchronized(锁升级、monitor、粒度)→ AQS(state + CLH 队列 + 模板方法,ReentrantLock/Semaphore/Latch 都基于它)→ 线程池(七参数、先排队后扩容、有界队列、拒绝策略、参数估算)→ 并发容器(CHM 桶锁 + compute 原子操作、COW 读多写少、阻塞队列)→ 工具类(Latch/Barrier/Semaphore/LongAdder)→ 排查(jstack 死锁、dump 看堆积、ThreadLocal remove)。核心心法:并发出问题八成是"以为原子其实不原子"或"以为可见其实不可见"——写代码时对每个共享变量问一句:谁在改、谁在读、靠什么保证。
第三篇讲 MongoDB。
(全文完)
(注:部分内容由豆包 AI 生成)
转载自 CSDN-专业IT技术社区
原文链接:https://blog.csdn.net/agoutongxie/article/details/170412159



