1. 宏观基石:进程、线程与内存模型

1.1 进程 vs 线程

  • 进程:操作系统资源分配的基本单位(拥有独立的堆内存、文件句柄)。进程间通信(IPC)需借助管道、消息队列,开销大。
  • 线程:CPU 调度的基本单位(共享进程资源,独有栈空间)。

1.2 上下文切换 (Context Switch)

当线程的时间片用完或被阻塞时,CPU 需保存当前线程的执行现场(寄存器、程序计数器)并加载新线程。

深度原理:切换涉及从用户态 (User Mode)内核态 (Kernel Mode) 的转换,消耗大量 CPU 周期。因此,高并发不等于线程越多越好,过多的线程会导致 CPU 忙于切换而非计算。

1.3 Java 内存模型 (JMM)

JMM 屏蔽了硬件差异,定义了线程与主存的交互规范:

  • 主内存:共享区域,存储所有变量。
  • 工作内存:线程私有,存储变量副本。

并发三大特性

  1. 原子性:操作不可分割(i++ 分为读、改、写,不具备原子性)。
  2. 可见性:修改后其他线程立即可见。
  3. 有序性:禁止指令重排。

1.4 Volatile 深度解析

volatile 是轻量级同步机制,保证可见性有序性,但不保证原子性。

  • 可见性原理:写操作后强制刷新主存,读操作前强制重载主存(基于内存屏障)。
  • 有序性原理:禁止 JVM 编译器和 CPU 的指令重排序。
经典案例:单例模式双重校验锁 (DCL)

Java

public class Singleton {
    private static volatile Singleton instance; // 必须 volatile

    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    // new 操作非原子,分为三步:
                    // 1. 分配内存
                    // 2. 初始化对象
                    // 3. 引用指向内存
                    instance = new Singleton(); 
                }
            }
        }
        return instance;
    }
}

为什么要 volatile?

如果不加,步骤 2 和 3 可能重排序。线程 A 执行了 1->3(此时对象非 null 但未初始化),线程 B 此时判断 instance != null,直接拿走了一个半成品对象,导致系统崩溃。


2. 锁的演进:CAS、Synchronized 与锁升级

2.1 CAS (Compare And Swap)

CAS 是乐观锁的基础(如 AtomicInteger)。

  • 原理:包含三个参数(内存地址 V,预期旧值 A,新值 B)。仅当 V==A 时,更新为 B。
  • 缺点
    • ABA 问题:值 A->B->A,CAS 无法感知(解法:加版本号)。
    • CPU 开销:自旋失败会一直占用 CPU。

2.2 Synchronized 与锁升级

JDK 1.6 后,Synchronized 不再单纯是重量级锁,引入了锁升级机制(存储在对象头 Mark Word 中):

  1. 偏向锁:只有一个线程访问,Mark Word 记录线程 ID,无同步开销。
  2. 轻量级锁:出现竞争,通过 CAS 自旋尝试获取锁(不阻塞)。
  3. 重量级锁:自旋失败,升级为操作系统互斥量(Mutex),线程挂起进入阻塞队列。

注意:锁只能升级,不能降级。


3. 架构核心:AQS 原理与 ReentrantLock 源码解析

AQS (AbstractQueuedSynchronizer) 是 JUC 包的基石。

3.1 AQS 核心三要素

  1. State (volatile int):同步状态。0=无锁,>0=有锁(体现可重入性)。
  2. Owner Thread:持有锁的线程。
  3. CLH 队列:双向链表实现的阻塞队列。

为什么用双向链表?

为了处理中断超时。当线程在排队时被取消,需要利用前驱指针将节点从链表中间快速移除。

3.2 ReentrantLock 源码剖析

ReentrantLock 默认非公平锁,性能优于公平锁。

(1) 加锁流程 (NonfairSync)
final void lock() {
    // 【步骤1】CAS 抢占(非公平体现:刚来就抢,不排队)
    if (compareAndSetState(0, 1))
        setExclusiveOwnerThread(Thread.currentThread());
    else
        acquire(1); // 抢占失败,走常规流程
}

public final void acquire(int arg) {
    // tryAcquire: 再次尝试获取
    // addWaiter: 放入队列尾部
    // acquireQueued: 阻塞线程(LockSupport.park)
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}
(2) 可重入逻辑 (nonfairTryAcquire)

Java

final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) { // 无锁,尝试 CAS
        if (compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    // 锁被占了,但持有者是自己(可重入)
    else if (current == getExclusiveOwnerThread()) {
        int nextc = c + acquires; // state + 1
        setState(nextc); // 无需 CAS,因为无竞争
        return true;
    }
    return false;
}
(3) 解锁流程 (tryRelease)

Java

protected final boolean tryRelease(int releases) {
    int c = getState() - releases;
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException(); // 只有持锁者才能解锁
    
    boolean free = false;
    if (c == 0) { // 只有 state 减到 0,才真正释放
        free = true;
        setExclusiveOwnerThread(null);
    }
    setState(c);
    return free; // 之后唤醒队列头结点的后继者
}

4. 资源调度:线程池设计与源码深度剖析

4.1 七大参数与配置

  1. corePoolSize:核心线程数。
  2. maximumPoolSize:最大线程数。
  3. keepAliveTime:非核心线程空闲存活时间。
  4. unit:时间单位。
  5. workQueue:阻塞队列。
  6. threadFactory:线程工厂(建议自定义命名)。
  7. handler:拒绝策略。

配置公式

  • CPU 密集型:CPU 核数 + 1
  • IO 密集型:CPU 核数 * 2(或 CPU / (1 - 阻塞系数))

4.2 ThreadPoolExecutor 源码剖析

(1) ctl 变量:位运算的艺术

Java

// 高3位表示状态(RUNNING等),低29位表示线程数
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));

一次 CAS 即可同时控制状态和数量,保证并发安全。

(2) execute 调度逻辑

Java

public void execute(Runnable command) {
    int c = ctl.get();
    // 1. 线程数 < 核心数:创建核心线程
    if (workerCountOf(c) < corePoolSize) {
        if (addWorker(command, true)) return;
        c = ctl.get();
    }
    // 2. 核心满了:放入队列
    if (isRunning(c) && workQueue.offer(command)) {
        int recheck = ctl.get();
        if (!isRunning(recheck) && remove(command)) reject(command);
        else if (workerCountOf(recheck) == 0) addWorker(null, false);
    }
    // 3. 队列满了:创建非核心线程(由 maxPoolSize 限制)
    else if (!addWorker(command, false))
        reject(command); // 4. 都不行:拒绝策略
}
(3) runWorker:线程复用原理

线程启动后进入 while 循环,不断从队列拿任务,这就是线程不死的秘密

Java

final void runWorker(Worker w) {
    try {
        // getTask() 是阻塞的:核心线程 take(),非核心 poll(time)
        while (task != null || (task = getTask()) != null) {
            w.lock();
            try {
                task.run(); // 直接调用 run,而非 start
            } finally {
                task = null;
            }
        }
    } finally {
        processWorkerExit(w, completedAbruptly);
    }
}

5. 生产实战:线上故障排查与隐患治理

5.1 线上 OOM (内存溢出)

  • 原因:使用 Executors.newFixedThreadPool,其底层队列是无界的 LinkedBlockingQueue。请求堆积导致堆爆满。
  • 解决:强制使用 ThreadPoolExecutor 手动创建,并设置有界队列拒绝策略(推荐 CallerRunsPolicy,由调用者执行任务,起削峰填谷作用)。

5.2 CPU 100% 排查步骤

  1. top:找到 CPU 最高的进程 PID。
  2. top -H -p PID:找到该进程下 CPU 最高的线程 TID。
  3. printf "%x\n" TID:转十六进制(如 0x1a)。
  4. jstack PID | grep 0x1a -A 20:查看堆栈。
    • 若状态是 RUNNABLE:检查死循环或复杂计算。
    • 若状态是 BLOCKED:检查锁竞争。

5.3 ThreadLocal 内存泄漏

  • 原理ThreadLocalMap 的 Key 是弱引用,Value 是强引用。线程长期存活(如线程池)时,Key 被 GC 回收为 null,但 Value 无法回收。

  • 解决:严格遵守 “谁 set,谁 remove”

    Java

    try {
        holder.set(val);
        // do something
    } finally {
        holder.remove(); // 必须清除
    }
    

5.4 死锁与预防

  • 条件:互斥、不可抢占、请求保持、循环等待。
  • 解决:破坏循环等待。
    • 案例:转账时(A->B, B->A),强制按账户 ID 大小顺序加锁(先锁小 ID,再锁大 ID)。

6. 工具对比:关键并发工具差异速查

维度 Synchronized Lock (ReentrantLock)
实现 JVM 关键字 (C++ Monitor) Java 类 (AQS)
释放 异常/结束自动释放 必须在 finallyunlock()
功能 只有非公平,不可中断 可中断、可公平/非公平
等待 wait/notify Condition (支持多路通知)
维度 sleep() wait()
来源 Thread 类 Object 类
锁处理 不释放锁 释放锁
场景 模拟延迟、轮询 线程间通信、同步
工具类 作用 场景
CountDownLatch 减法计数,等 N 个任务完成 启动服务前等待组件加载
CyclicBarrier 加法计数,集齐 N 个线程才出发 多线程计算后合并结果
Semaphore 信号量,控制并发数 限流(如数据库连接池)
Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐