Java并发编程面试全攻略-从synchronized到AQS底层原理

并发编程是Java面试的重灾区。这篇文章帮你建立完整的并发知识图谱。

前言

并发编程大概是Java面试中最"玄学"的部分------知识点多、概念抽象、还容易搞混。

但其实并发问题的本质就三个词:原子性、可见性、有序性。所有的并发工具和机制,都是在解决这三个问题。搞清楚这个主线,面试时就能以不变应万变。


一、并发问题的本质

为什么会有并发问题?

复制代码
根本原因:CPU、内存、IO的速度差异

CPU >> 内存 >> IO

为了弥补速度差异,硬件和编译器做了优化:
1. CPU缓存 → 导致可见性问题
2. 线程切换 → 导致原子性问题
3. 编译优化(指令重排) → 导致有序性问题

Java内存模型(JMM)

复制代码
线程A                    线程B
┌──────────┐            ┌──────────┐
│ 工作内存  │            │ 工作内存  │
│ (CPU缓存) │            │ (CPU缓存) │
└─────┬────┘            └────┬─────┘
      │    read/write         │
      ├──────────┬────────────┤
      │          ▼            │
      │   ┌───────────┐      │
      │   │  主内存     │      │
      │   │ (堆/变量)  │      │
      │   └───────────┘      │
      
问题:线程A修改了变量x,线程B可能读到旧值(缓存未同步)
解决:volatile、synchronized、Lock

二、synchronized深入

synchronized的三种用法

java 复制代码
// 1. 修饰实例方法 → 锁的是this对象
public synchronized void method() { }

// 2. 修饰静态方法 → 锁的是Class对象
public static synchronized void method() { }

// 3. 修饰代码块 → 锁的是指定对象
synchronized (lockObj) { }

synchronized的锁升级过程(JDK6+优化)

复制代码
无锁 → 偏向锁 → 轻量级锁 → 重量级锁
(只能升级,不能降级)

1. 偏向锁:
   - 场景:只有一个线程访问
   - 实现:在Mark Word中记录线程ID
   - 第二次进入同步块时,对比线程ID即可,无需CAS
   - 升级条件:有其他线程来竞争

2. 轻量级锁:
   - 场景:多线程交替执行,没有真正竞争
   - 实现:CAS将Mark Word复制到栈帧的Lock Record
   - 不会阻塞线程(自旋等待)
   - 升级条件:自旋超过一定次数/自旋线程数超过CPU核心数一半

3. 重量级锁:
   - 场景:真正的并发竞争
   - 实现:通过Monitor(ObjectMonitor)实现
   - 获取不到锁的线程会被阻塞(涉及用户态→内核态切换)

面试追问:synchronized和Lock的区别?

维度 synchronized ReentrantLock
实现 JVM层面(monitorenter/exit) API层面(AQS)
加锁释放 自动释放 手动unlock(必须finally)
可中断 不支持 lockInterruptibly()
超时 不支持 tryLock(timeout)
公平性 非公平 可选公平/非公平
条件变量 wait/notify(单个) Condition(多个)
性能 JDK6后优化好了,差不多 差不多

三、volatile关键字

volatile的作用

java 复制代码
// 1. 保证可见性
volatile boolean flag = false;
// 线程A修改flag=true后,线程B能立即看到

// 2. 禁止指令重排
// 经典案例:双重检查锁的单例模式
public class Singleton {
    private static volatile Singleton instance; // 必须加volatile!
    
    public static Singleton getInstance() {
        if (instance == null) {                    // 第一次检查
            synchronized (Singleton.class) {
                if (instance == null) {            // 第二次检查
                    instance = new Singleton();    // 可能重排序!
                }
            }
        }
        return instance;
    }
}

为什么单例要加volatile?

复制代码
instance = new Singleton() 实际分三步:
1. 分配内存空间
2. 初始化对象
3. 将引用指向内存空间

指令重排后可能变成 1→3→2:
- 线程A执行了1和3,还没执行2
- 线程B判断instance != null,直接返回
- 线程B拿到了一个未初始化的对象 → 出bug

volatile禁止重排 → 保证顺序是1→2→3

volatile不保证原子性

java 复制代码
volatile int count = 0;

// ❌ 这不是线程安全的!
count++; // 实际是 read → +1 → write 三步操作

// ✅ 要保证原子性,用AtomicInteger
AtomicInteger count = new AtomicInteger(0);
count.incrementAndGet(); // CAS原子操作

四、CAS与原子类

CAS原理

复制代码
CAS(Compare And Swap):比较并交换

操作:CAS(内存地址V, 期望值A, 新值B)
含义:如果V的值==A,就把V改为B,返回true
     如果V的值!=A(被别人改了),不修改,返回false

底层:CPU指令(cmpxchg),硬件级别保证原子性

AtomicInteger源码核心

java 复制代码
public class AtomicInteger {
    private volatile int value; // volatile保证可见性
    
    public final int incrementAndGet() {
        // Unsafe.getAndAddInt → 自旋CAS
        return U.getAndAddInt(this, VALUE, 1) + 1;
    }
}

// Unsafe.getAndAddInt的实现
public final int getAndAddInt(Object o, long offset, int delta) {
    int v;
    do {
        v = getIntVolatile(o, offset); // 读取当前值
    } while (!compareAndSwapInt(o, offset, v, v + delta)); // CAS直到成功
    return v;
}

CAS的三大问题

复制代码
问题1:ABA问题
- A→B→A,CAS认为没变过
- 解决:AtomicStampedReference(版本号)

问题2:自旋开销
- 高竞争下一直CAS失败,浪费CPU
- 解决:LongAdder(分段CAS,减少竞争)

问题3:只能保证单个变量的原子性
- 多个变量怎么办?
- 解决:AtomicReference(把多个变量封装为对象)

五、AQS框架(AbstractQueuedSynchronizer)

AQS是什么

复制代码
AQS = 抽象队列同步器
是Java并发包的基石:

基于AQS的实现:
- ReentrantLock
- ReentrantReadWriteLock
- Semaphore
- CountDownLatch
- CyclicBarrier

AQS核心设计

复制代码
核心组成:
1. state变量(volatile int):表示同步状态
   - ReentrantLock中:0=未锁定,>0=锁定(可重入次数)
   - Semaphore中:剩余许可数
   - CountDownLatch中:剩余计数

2. CLH队列(双向链表):存放等待的线程
   
   head ←→ Node(Thread1) ←→ Node(Thread2) ←→ tail

3. 两种模式:
   - 独占模式(Exclusive):只有一个线程能获取锁(如ReentrantLock)
   - 共享模式(Shared):多个线程可同时获取(如Semaphore)

AQS获取锁流程(独占模式)

java 复制代码
// ReentrantLock.lock() 最终调用 AQS.acquire()
public final void acquire(int arg) {
    if (!tryAcquire(arg) &&              // 1. 尝试获取锁(子类实现)
        acquireQueued(                    // 3. 在队列中自旋/阻塞
            addWaiter(Node.EXCLUSIVE),    // 2. 获取失败,加入等待队列
            arg))
        selfInterrupt();
}

// 非公平锁的tryAcquire
protected final boolean tryAcquire(int acquires) {
    Thread current = Thread.currentThread();
    int c = getState();
    
    if (c == 0) {
        // 锁空闲,CAS尝试获取
        if (compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    } else if (current == getExclusiveOwnerThread()) {
        // 可重入:当前线程已持有锁
        setState(c + acquires);
        return true;
    }
    return false;
}

公平锁 vs 非公平锁

java 复制代码
// 公平锁:新来的线程必须排队
if (c == 0) {
    if (!hasQueuedPredecessors() &&  // 队列中没有等待的线程才尝试
        compareAndSetState(0, acquires)) {
        ...
    }
}

// 非公平锁:新来的线程直接尝试CAS,失败了才排队
if (c == 0) {
    if (compareAndSetState(0, acquires)) { // 直接抢
        ...
    }
}

// 为什么默认非公平?
// 性能好!减少了线程唤醒的开销
// 新来的线程如果能直接拿到锁,就不用进队列→挂起→唤醒

六、线程池

线程池参数详解

java 复制代码
public ThreadPoolExecutor(
    int corePoolSize,        // 核心线程数(即使空闲也不回收)
    int maximumPoolSize,     // 最大线程数
    long keepAliveTime,      // 非核心线程空闲多久回收
    TimeUnit unit,           // 时间单位
    BlockingQueue<Runnable> workQueue,  // 任务队列
    ThreadFactory threadFactory,        // 线程工厂
    RejectedExecutionHandler handler    // 拒绝策略
)

任务提交流程

复制代码
提交任务
   │
   ▼
核心线程数满了吗? ──否──→ 创建核心线程执行
   │是
   ▼
任务队列满了吗? ──否──→ 放入队列等待
   │是
   ▼
达到最大线程数了吗? ──否──→ 创建非核心线程执行
   │是
   ▼
执行拒绝策略

面试常问:线程池参数怎么设置?

java 复制代码
// CPU密集型任务(计算多、IO少)
int corePoolSize = Runtime.getRuntime().availableProcessors() + 1;

// IO密集型任务(等待IO多、计算少)
int corePoolSize = Runtime.getRuntime().availableProcessors() * 2;

// 混合型(推荐公式)
int corePoolSize = Ncpu * Ucpu * (1 + W/C);
// Ncpu = CPU核心数
// Ucpu = 目标CPU利用率(0-1)
// W/C = 等待时间/计算时间

// 实际生产中:压测!理论公式只是起点

四种拒绝策略

java 复制代码
// 1. AbortPolicy(默认):抛RejectedExecutionException
// 2. CallerRunsPolicy:调用者线程执行(降级但不丢任务)
// 3. DiscardPolicy:静默丢弃
// 4. DiscardOldestPolicy:丢弃队列中最老的任务

// 生产建议:自定义拒绝策略
new RejectedExecutionHandler() {
    @Override
    public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
        // 1. 记录日志
        log.error("线程池已满,任务被拒绝: {}", r.toString());
        // 2. 持久化到数据库/MQ,后续补偿
        saveToRetryQueue(r);
        // 3. 告警
        alertService.notify("线程池任务拒绝");
    }
};

七、并发工具类

CountDownLatch(倒计时器)

java 复制代码
// 场景:主线程等待N个子任务完成
CountDownLatch latch = new CountDownLatch(3);

// 子任务执行完调用 countDown()
executor.submit(() -> {
    doTask1();
    latch.countDown(); // 计数-1
});
executor.submit(() -> {
    doTask2();
    latch.countDown();
});
executor.submit(() -> {
    doTask3();
    latch.countDown();
});

// 主线程等待
latch.await(); // 阻塞直到计数为0
System.out.println("所有任务完成");

CyclicBarrier(循环栅栏)

java 复制代码
// 场景:N个线程相互等待,到齐后一起继续
CyclicBarrier barrier = new CyclicBarrier(3, () -> {
    System.out.println("所有线程到达屏障,开始下一阶段");
});

// 每个线程执行到await()时等待
executor.submit(() -> {
    phase1();
    barrier.await(); // 等待其他线程
    phase2();        // 到齐后继续
});

Semaphore(信号量)

java 复制代码
// 场景:限制同时访问的线程数(如数据库连接池)
Semaphore semaphore = new Semaphore(10); // 最多10个线程同时访问

public void accessDatabase() {
    semaphore.acquire(); // 获取许可(没有则阻塞)
    try {
        doQuery();
    } finally {
        semaphore.release(); // 释放许可
    }
}

三者对比

工具 计数变化 可重用 典型场景
CountDownLatch 减到0 ❌一次性 主线程等待子任务
CyclicBarrier 到齐后重置 ✅可循环 多阶段并行任务
Semaphore acquire减/release加 限流/连接池

八、ThreadLocal

原理

java 复制代码
// 每个Thread对象有一个ThreadLocalMap
// key = ThreadLocal对象(弱引用)
// value = 实际存储的值

public class Thread {
    ThreadLocal.ThreadLocalMap threadLocals; // 每个线程独立的Map
}

// set时:
public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = t.threadLocals;
    map.set(this, value); // this = 当前ThreadLocal对象
}

内存泄漏问题(面试必问)

复制代码
ThreadLocalMap的Entry:
key = ThreadLocal的弱引用
value = 实际值的强引用

当ThreadLocal对象被GC回收后:
key变为null,但value还在!
→ 这个Entry永远不会被访问到,但也不会被回收 → 内存泄漏

解决:用完之后必须调用 threadLocal.remove()

// 特别是在线程池中:
try {
    threadLocal.set(user);
    doSomething();
} finally {
    threadLocal.remove(); // 必须remove!线程会被复用!
}

九、面试高频连环问

Q: 线程安全的实现方式有哪些?

复制代码
1. 不可变对象:String、Integer(天生线程安全)
2. synchronized/Lock:互斥同步
3. CAS + volatile:无锁并发(Atomic类)
4. ThreadLocal:线程封闭(每个线程有自己的副本)
5. 并发容器:ConcurrentHashMap、CopyOnWriteArrayList

Q: 死锁的四个条件和排查方法?

复制代码
四个必要条件:
1. 互斥:资源不能共享
2. 持有并等待:持有一个资源同时等待另一个
3. 不可剥夺:不能强行抢走别人的资源
4. 循环等待:A等B,B等A

排查方法:
1. jstack <pid> → 看有没有 "Found one Java-level deadlock"
2. Arthas的 thread -b → 找到阻塞的线程
3. VisualVM/JConsole → 图形化检测死锁

预防方法:
1. 按固定顺序加锁
2. 设置超时时间(tryLock(timeout))
3. 使用Lock而不是synchronized(可以中断和超时)

写在最后

Java并发的核心知识点:

复制代码
底层原理:JMM、happens-before、volatile、CAS
锁机制:synchronized(锁升级)、AQS(ReentrantLock)
线程池:参数含义、执行流程、拒绝策略
工具类:CountDownLatch、CyclicBarrier、Semaphore
线程安全:ThreadLocal、并发容器、原子类

面试时能把每个点讲清楚"是什么→为什么→怎么用→有什么坑",就够了。


并发是Java面试重中之重,建议收藏反复看!下篇写Spring全家桶~

相关推荐
云泽8081 小时前
Python 开发环境搭建全指南:从 Python 安装到 PyCharm 配置详解
开发语言·python·pycharm
不会代码的小猴1 小时前
2. 了解Qt
开发语言·c++·笔记·qt·算法
ZC跨境爬虫1 小时前
LeetCode 119. 杨辉三角 II(原地更新优化详解 + Java Python 实现)
java·python·leetcode
ZJU_统一阿萨姆1 小时前
【推理优化进阶】Hopper_Blackwell 微架构:从指令、流水线到真实性能上限
开发语言·人工智能·语言模型·架构·开源
程序员爱钓鱼1 小时前
Go 编程实战:数组 Array——固定长度的数据集合
后端·面试·go
程序喵大人3 小时前
【C++进阶】STL算法与函数对象 - 09 函数对象保存状态并复用规则
开发语言·c++·算法·stl·函数对象
程序员黑豆7 小时前
Java包装类:基本类型与对象的桥梁
java·前端·ai编程
ShineWinsu9 小时前
对于C++:auto_ptr、unique_ptr、shared_ptr的模拟实现
c++·面试·笔试·智能指针·unique_ptr·shared_ptr·auto_ptr