阿狗童鞋头像

Java并发编程实战指南 (1)

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 对比

维度synchronizedReentrantLock
实现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

十、常见坑速查

现象原因解决
标志位不生效无 volatilevolatile
i++ 丢更新非原子Atomic/synchronized
单例半成品DCL 无 volatilevolatile
死锁交叉加锁固定顺序/tryLock
池 OOM无界队列有界 + 拒绝策略
ThreadLocal 泄漏未 removefinally remove
CHM 数据错复合操作非原子computeIfAbsent
计数慢热点 CASLongAdder
任务丢DiscardPolicyCallerRuns/自定义

本章小结

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

文章来源转载

评论

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--