找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

4244

积分

0

好友

582

主题
发表于 1 小时前 | 查看: 1| 回复: 0

面试官:"请说一下Java内存模型,volatile能保证原子性吗?synchronized的锁升级过程是怎样的?AQS的原理是什么?线程池的核心参数怎么设置?"

:(从容地翻开笔记本,从内存模型开始层层递进...)

考察重点

这道题是中高级Java面试的必考重难点,面试官想考察:

  1. 底层理解:对Java内存模型、CPU缓存一致性、指令重排序的理解深度
  2. 原理掌握:synchronized的优化历程、AQS的设计思想
  3. 实战能力:线程池参数设置、并发工具类的正确使用
  4. 源码功底:是否读过AQS、ReentrantLock、ThreadPoolExecutor源码

一、Java内存模型(JMM)与volatile

1.1 为什么需要Java内存模型?

硬件层面的问题:CPU和内存之间速度差异巨大,引入了CPU缓存(L1/L2/L3)、写缓冲器、无效化队列等硬件优化,导致可见性问题有序性问题

Java内存模型(JMM):屏蔽不同硬件和操作系统的内存访问差异,保证Java程序在各种平台下都能达到一致的内存访问效果。

JMM的核心概念

  • 主内存:所有变量都存储在主内存中(所有线程共享)
  • 工作内存:每个线程有自己的工作内存,保存了被该线程使用的变量的主内存副本拷贝
  • 线程对变量的所有操作必须在工作内存中进行,不能直接读写主内存
┌─────────────┐    ┌─────────────┐    ┌─────────────┐
│   Thread1   │    │   Thread2   │    │   Thread3   │
├─────────────┤    ├─────────────┤    ├─────────────┤
│ 工作内存    │    │ 工作内存    │    │ 工作内存    │
│  变量副本   │    │  变量副本   │    │  变量副本   │
└──────┬──────┘    └──────┬──────┘    └──────┬──────┘
       │                  │                  │
       └──────────────────┼──────────────────┘
                          ▼
                ┌─────────────────┐
                │    主内存       │
                │   共享变量      │
                └─────────────────┘

1.2 volatile的内存语义

核心作用

  1. 保证可见性:对volatile变量的写操作会立即刷新到主内存,读操作会从主内存读取最新值
  2. 禁止指令重排序:在volatile变量前后插入内存屏障,防止编译器、CPU对指令重排序

volatile写-读建立的happens-before关系

  • 对一个volatile变量的写操作,happens-before于后续对这个volatile变量的读操作
  • 这意味着:线程A写volatile变量,线程B读同一个volatile变量,那么线程A在写之前的所有操作的结果对线程B都是可见的

1.3 volatile的实现原理

字节码层面

public class VolatileExample {
    private volatile boolean flag = false;

    public void setFlag() {
        flag = true; // 字节码: putstatic 字段
    }
}

JVM层面:编译成字节码后,JVM会在volatile变量操作时添加内存屏障(Memory Barrier)

CPU层面:在x86架构下,volatile写操作会在硬件层面通过MESI缓存一致性协议 + Lock前缀指令实现。

x86下的volatile写实现lock addl $0x0, (%rsp)):

  1. 将当前处理器缓存行的数据写回到系统内存
  2. 这个写回内存的操作会使在其他CPU里缓存了该内存地址的数据无效(通过总线嗅探机制)

内存屏障策略(JMM规范):

屏障类型 指令示例 说明
LoadLoad Load1; LoadLoad; Load2 确保Load1的数据加载先于Load2
StoreStore Store1; StoreStore; Store2 确保Store1的数据刷入内存先于Store2
LoadStore Load1; LoadStore; Store2 确保Load1的数据加载先于Store2写入
StoreLoad Store1; StoreLoad; Load2 确保Store1的数据刷入内存先于Load2加载(开销最大)

volatile读/写插入的屏障

  • volatile读:LoadLoad屏障(后续读不能重排到前面)
  • volatile写:StoreStore屏障(前面写不能重排到后面)+ StoreLoad屏障(防止写和后续读重排)

1.4 volatile能保证原子性吗?

不能! volatile只能保证单次读/写的原子性(对基本类型的读写是原子性的),但不能保证复合操作的原子性。

public class VolatileAtomicTest {
    private volatile int count = 0;

    public void increment() {
        count++; // 非原子操作:读-改-写
    }

    public static void main(String[] args) throws InterruptedException {
        VolatileAtomicTest test = new VolatileAtomicTest();
        for (int i = 0; i < 1000; i++) {
            new Thread(test::increment).start();
        }
        Thread.sleep(3000);
        System.out.println(test.count); // 结果可能小于1000
    }
}
1. 从内存读取count到寄存器(read)
2. 寄存器中+1操作(modify)
3. 将值写回内存(write)

解决方案:使用synchronizedReentrantLockAtomicInteger

private AtomicInteger count = new AtomicInteger(0);
public void increment() {
    count.incrementAndGet(); // CAS保证原子性
}

1.5 volatile vs synchronized

特性 volatile synchronized
可见性 ✅ 保证 ✅ 保证
原子性 ❌ 不保证(仅单次读写) ✅ 保证
互斥性 ❌ 无 ✅ 互斥锁
阻塞性 非阻塞 阻塞
性能 轻量级(无锁) 重量级(锁竞争时)
适用场景 单一变量的可见性控制 复合操作、代码块互斥

二、synchronized的锁升级过程

2.1 锁的状态

JDK 1.6对synchronized进行了大量优化,引入了锁升级机制,锁的状态从低到高分为四种(无锁、偏向锁、轻量级锁、重量级锁),锁只能升级,不能降级

无锁 → 偏向锁 → 轻量级锁 → 重量级锁

2.2 对象头结构

锁状态信息存储在Java对象的对象头(Mark Word)中。64位JVM的Mark Word结构:

锁状态 25bit 31bit 1bit 4bit 1bit 2bit
无锁 unused hashCode 0 分代年龄 0 01
偏向锁 threadId(54bit) epoch(2bit) 1 分代年龄 0 01
轻量级锁 指向栈中锁记录的指针(62bit) 00
重量级锁 指向互斥量(重量级锁)的指针(62bit) 10
GC标记 11

2.3 偏向锁

场景:锁总是由同一个线程多次获取(如Vector、StringBuffer等线程安全类)

原理:当锁对象第一次被线程获取时,JVM将对象头的Mark Word设置为偏向模式,记录线程ID。之后该线程再次进入同步块时,只需比较线程ID,无需CAS操作。

偏向锁撤销:当有其他线程尝试竞争锁时,偏向锁升级为轻量级锁。

JVM参数

# 开启偏向锁(JDK6默认开启,JDK15后默认关闭)
-XX:+UseBiasedLocking
# 偏向锁启动延迟(默认4秒)
-XX:BiasedLockingStartupDelay=0

2.4 轻量级锁

场景:多个线程交替执行,没有真正的竞争

原理

  1. 线程A进入同步块,JVM在当前线程的栈帧中创建锁记录(Lock Record)
  2. 将对象头的Mark Word复制到锁记录中(Displaced Mark Word
  3. 使用CAS操作将对象头的Mark Word替换为指向锁记录的指针
  4. 如果CAS成功,线程A获得锁;如果失败,说明有竞争,膨胀为重量级锁

轻量级锁解锁:使用CAS将Displaced Mark Word替换回对象头

2.5 重量级锁

场景:多个线程同时竞争锁

原理:通过操作系统的互斥量(mutex)实现,线程阻塞和唤醒需要从用户态切换到内核态,开销较大。

内部实现:依赖于monitor对象(每个Java对象都有一个monitor),使用monitorentermonitorexit指令。

2.6 锁升级流程图

┌─────────────────────────────────────────────────────────────┐
│                        无锁状态                              │
└────────────────────────────┬────────────────────────────────┘
                            │ 第一个线程进入同步块
                            ▼
┌─────────────────────────────────────────────────────────────┐
│                        偏向锁状态                            │
│  - 记录线程ID到对象头                                        │
│  - 后续同线程进入无CAS操作                                   │
└────────────────────────────┬────────────────────────────────┘
                            │ 其他线程尝试获取锁
                            ▼
┌─────────────────────────────────────────────────────────────┐
│                        轻量级锁状态                          │
│  - CAS尝试获取锁,失败则自旋                                 │
│  - 自旋次数限制(默认10次)                                  │
└────────────────────────────┬────────────────────────────────┘
                            │ 自旋超过限制/有线程自旋等待
                            ▼
┌─────────────────────────────────────────────────────────────┐
│                        重量级锁状态                          │
│  - 线程阻塞,进入等待队列                                    │
│  - 依赖操作系统mutex                                        │
└─────────────────────────────────────────────────────────────┘

2.7 锁优化的其他手段

自旋锁(Spin Lock):线程不会立即阻塞,而是执行一段忙循环等待锁释放。适用于锁持有时间短的场景。

自适应自旋:JDK6引入,自旋次数根据上一次在同一锁上的自旋时间动态调整。

锁消除(Lock Elimination):JIT编译器检测到不存在竞争时,消除锁。如StringBuffer作为局部变量时。

锁粗化(Lock Coarsening):将多个连续的加锁解锁操作合并为一次,减少锁操作次数。

三、AQS框架源码深度剖析

3.1 什么是AQS?

AbstractQueuedSynchronizer(AQS) 是Java并发包(JUC)的核心基础框架,它提供了一个FIFO队列同步状态管理的模板,ReentrantLockCountDownLatchSemaphoreReentrantReadWriteLock等都是基于AQS实现的。

核心设计思想

  • 同步状态(state):用volatile int表示,通过getState()setState()compareAndSetState()操作
  • CLH队列:基于双向链表的FIFO等待队列,存放等待获取锁的线程节点
  • 模板方法模式:子类只需实现tryAcquire()tryRelease()等方法

3.2 AQS核心数据结构

// AQS中的Node(等待队列节点)
static final class Node {
    // 节点状态
    static final int CANCELLED =  1; // 取消
    static final int SIGNAL    = -1; // 后继节点需要被唤醒
    static final int CONDITION = -2; // 在条件队列中等待
    static final int PROPAGATE = -3; // 共享模式下,传播释放

    volatile int waitStatus; // 等待状态
    volatile Node prev;      // 前驱节点
    volatile Node next;      // 后继节点
    volatile Thread thread;  // 当前节点持有的线程
    Node nextWaiter;         // 条件队列的下一个节点
}

// AQS核心字段
public abstract class AbstractQueuedSynchronizer {
    private transient volatile Node head; // 队列头节点
    private transient volatile Node tail; // 队列尾节点
    private volatile int state;           // 同步状态
}

3.3 独占模式(以ReentrantLock为例)

获取锁的流程

// 入口:ReentrantLock.lock()
public void lock() {
    sync.acquire(1);
}

// AQS.acquire()
public final void acquire(int arg) {
    // 1. tryAcquire:尝试获取锁(子类实现)
    // 2. addWaiter:失败则加入等待队列
    // 3. acquireQueued:在队列中自旋获取锁
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

// 创建节点并加入队列
private Node addWaiter(Node mode) {
    Node node = new Node(Thread.currentThread(), mode);
    Node pred = tail;
    if (pred != null) {
        node.prev = pred;
        // CAS设置尾节点
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    enq(node); // 初始化队列或CAS失败时自旋入队
    return node;
}

// 在队列中自旋获取锁
final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {
            final Node p = node.predecessor();
            // 只有前驱是头节点时才尝试获取锁
            if (p == head && tryAcquire(arg)) {
                setHead(node); // 设置当前节点为头节点
                p.next = null;
                failed = false;
                return interrupted;
            }
            // 检查是否需要阻塞
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

释放锁的流程

// 入口:ReentrantLock.unlock()
public void unlock() {
    sync.release(1);
}

// AQS.release()
public final boolean release(int arg) {
    if (tryRelease(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h); // 唤醒后继节点
        return true;
    }
    return false;
}

// 唤醒后继节点
private void unparkSuccessor(Node node) {
    int ws = node.waitStatus;
    if (ws < 0)
        compareAndSetWaitStatus(node, ws, 0);

    Node s = node.next;
    // 如果后继节点被取消或为空,从尾部向前找第一个有效节点
    if (s == null || s.waitStatus > 0) {
        s = null;
        for (Node t = tail; t != null && t != node; t = t.prev)
            if (t.waitStatus <= 0)
                s = t;
    }
    if (s != null)
        LockSupport.unpark(s.thread);
}

3.4 共享模式(以CountDownLatch为例)

共享模式的核心区别:多个线程可以同时获取同步状态

// CountDownLatch的await()
public void await() throws InterruptedException {
    sync.acquireSharedInterruptibly(1);
}

public final void acquireSharedInterruptibly(int arg) {
    if (Thread.interrupted())
        throw new InterruptedException();
    // 尝试获取共享锁
    if (tryAcquireShared(arg) < 0)
        doAcquireSharedInterruptibly(arg);
}

private void doAcquireSharedInterruptibly(int arg) {
    final Node node = addWaiter(Node.SHARED);
    try {
        for (;;) {
            final Node p = node.predecessor();
            if (p == head) {
                int r = tryAcquireShared(arg);
                if (r >= 0) {
                    setHeadAndPropagate(node, r); // 设置头并传播
                    p.next = null;
                    return;
                }
            }
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                throw new InterruptedException();
        }
    } catch (Throwable t) {
        cancelAcquire(node);
        throw t;
    }
}

// CountDownLatch的countDown()
public void countDown() {
    sync.releaseShared(1);
}

public final boolean releaseShared(int arg) {
    if (tryReleaseShared(arg)) {
        doReleaseShared(); // 唤醒所有等待线程
        return true;
    }
    return false;
}

3.5 Condition实现原理

Condition基于AQS的条件队列实现,每个Condition对象对应一个条件队列。

// await()流程
public final void await() throws InterruptedException {
    // 1. 创建节点加入条件队列
    Node node = addConditionWaiter();
    // 2. 释放锁(完全释放)
    int savedState = fullyRelease(node);
    // 3. 阻塞,直到被signal
    while (!isOnSyncQueue(node)) {
        LockSupport.park(this);
    }
    // 4. 重新获取锁
    acquireQueued(node, savedState);
}

// signal()流程
public final void signal() {
    // 1. 从条件队列头部取出节点
    Node first = firstWaiter;
    if (first != null)
        doSignal(first);
}

private void doSignal(Node first) {
    do {
        // 2. 从条件队列移除
        if ((firstWaiter = first.nextWaiter) == null)
            lastWaiter = null;
        first.nextWaiter = null;
        // 3. 转移到同步队列
    } while (!transferForSignal(first) &&
             (first = firstWaiter) != null);
}

四、线程池的核心参数和拒绝策略

4.1 线程池核心参数

public ThreadPoolExecutor(int corePoolSize,           // 核心线程数
                          int maximumPoolSize,        // 最大线程数
                          long keepAliveTime,         // 空闲线程存活时间
                          TimeUnit unit,              // 时间单位
                          BlockingQueue<Runnable> workQueue,  // 任务队列
                          ThreadFactory threadFactory,        // 线程工厂
                          RejectedExecutionHandler handler)   // 拒绝策略
参数详解 参数 作用 设置建议
corePoolSize 核心线程数,即使空闲也不会回收 CPU密集型:CPU核心数+1
IO密集型:CPU核心数×2
maximumPoolSize 最大线程数 队列满时才创建额外线程
keepAliveTime 非核心线程空闲存活时间 根据业务场景(30s~5min)
workQueue 任务队列 见下表
threadFactory 自定义线程创建(命名、优先级) 必设!便于问题排查
handler 队列满且线程池满时的拒绝策略 根据业务场景选择
任务队列类型 队列类型 特点 适用场景
SynchronousQueue 不存储任务,直接交给线程 无界线程池(CachedThreadPool)
LinkedBlockingQueue 无界队列(默认容量Integer.MAX) 任务量可控,避免OOM
ArrayBlockingQueue 有界队列 任务量可控,需要限制积压
DelayQueue 延迟任务队列 定时/延迟任务

4.2 线程池执行流程

提交任务
    │
    ▼
当前线程数 < corePoolSize?
    │
    ├── 是 ──→ 创建新线程执行
    │
    └── 否 ──→ 任务队列是否已满?
                │
                ├── 否 ──→ 加入队列等待
                │
                └── 是 ──→ 当前线程数 < maximumPoolSize?
                            │
                            ├── 是 ──→ 创建新线程执行
                            │
                            └── 否 ──→ 执行拒绝策略

4.3 拒绝策略

JDK内置四种拒绝策略: 策略 行为 适用场景
AbortPolicy(默认) 抛出RejectedExecutionException 关键业务,必须处理
CallerRunsPolicy 由调用线程执行任务 降级策略,保证任务不丢失
DiscardPolicy 丢弃任务,不抛异常 非核心业务
DiscardOldestPolicy 丢弃队列头任务,重试提交 追求新任务的处理

4.4 线程池的正确使用

常见错误

// ❌ 错误1:使用Executors快捷方法(可能导致OOM)
ExecutorService executor = Executors.newFixedThreadPool(10);
// LinkedBlockingQueue无界,任务积压会导致OOM

// ❌ 错误2:不设置ThreadFactory
// 无法定位问题线程,线程名称无意义

// ❌ 错误3:没有优雅关闭
// 任务可能丢失或强制中断

正确使用

// ✅ 推荐:手动创建ThreadPoolExecutor
ThreadPoolExecutor executor = new ThreadPoolExecutor(
    10,                           // 核心线程数
    20,                           // 最大线程数
    60, TimeUnit.SECONDS,         // 空闲存活时间
    new ArrayBlockingQueue<>(1000), // 有界队列
    new ThreadFactoryBuilder()     // Guava的ThreadFactoryBuilder
        .setNameFormat("biz-pool-%d")
        .setDaemon(false)
        .build(),
    new ThreadPoolExecutor.CallerRunsPolicy() // 降级策略
);

// 优雅关闭
executor.shutdown(); // 不再接收新任务,等待已提交任务执行
try {
    if (!executor.awaitTermination(60, TimeUnit.SECONDS)) {
        executor.shutdownNow(); // 超时则强制关闭
    }
} catch (InterruptedException e) {
    executor.shutdownNow();
    Thread.currentThread().interrupt();
}

五、实战:并发工具类的正确使用姿势

5.1 并发容器选型

场景 推荐容器 原因
读多写少 CopyOnWriteArrayList 写时复制,读无锁
单线程读+写 ConcurrentHashMap 分段锁/CAS,高并发
需要排序 ConcurrentSkipListMap 跳表实现,线程安全
生产者消费者 ArrayBlockingQueue 有界阻塞队列
延迟任务 DelayQueue 按延迟时间取出

5.2 线程安全工具类

// 1. AtomicInteger:CAS实现,原子操作
AtomicInteger counter = new AtomicInteger(0);
counter.incrementAndGet(); // 原子+1

// 2. LongAdder:高并发下性能优于AtomicLong
LongAdder adder = new LongAdder();
adder.add(1);
adder.sum();

// 3. CountDownLatch:等待多个任务完成
CountDownLatch latch = new CountDownLatch(5);
for (int i = 0; i < 5; i++) {
    executor.execute(() -> {
        // 业务逻辑
        latch.countDown();
    });
}
latch.await(10, TimeUnit.SECONDS);

// 4. CyclicBarrier:循环屏障,多线程等待到齐后同时执行
CyclicBarrier barrier = new CyclicBarrier(5, () -> {
    System.out.println("所有线程准备就绪,开始执行");
});
for (int i = 0; i < 5; i++) {
    executor.execute(() -> {
        // 准备工作
        barrier.await(); // 等待所有线程到齐
        // 同时执行
    });
}

// 5. Semaphore:限流控制
Semaphore semaphore = new Semaphore(10); // 最大并发10
try {
    semaphore.acquire();
    // 业务逻辑
} finally {
    semaphore.release();
}

// 6. CompletableFuture:异步编排
CompletableFuture<String> future = CompletableFuture
    .supplyAsync(() -> queryData())
    .thenApply(data -> processData(data))
    .thenApply(result -> formatResult(result))
    .exceptionally(ex -> "error: " + ex.getMessage());

5.3 死锁排查与预防

死锁四个必要条件

  1. 互斥条件:资源不能被共享
  2. 请求与保持:持有一个资源并等待其他资源
  3. 不可剥夺:已获得资源不可强行剥夺
  4. 循环等待:多个线程形成循环等待链

排查方法

# 1. 查看Java进程
jps -l

# 2. 打印线程堆栈,查找deadlock信息
jstack <pid>

# 输出示例:
# Found one Java-level deadlock:
# "Thread-0":
#   waiting for ownable synchronizer 0x000000071a123456,
#   held by "Thread-1"
# "Thread-1":
#   waiting for ownable synchronizer 0x000000071a123457,
#   held by "Thread-0"

预防策略

  • 按固定顺序获取锁
  • 使用tryLock()设置超时
  • 减少锁粒度
  • 使用并发容器替代手动加锁

六、思维导图总结

并发编程核心
├── Java内存模型(JMM)
│   ├── 主内存 vs 工作内存
│   ├── happens-before规则
│   └── volatile:可见性、禁止重排序
├── 锁机制
│   ├── synchronized锁升级
│   │   ├── 偏向锁
│   │   ├── 轻量级锁
│   │   └── 重量级锁
│   └── AQS框架
│       ├── state + CLH队列
│       ├── 独占模式(ReentrantLock)
│       ├── 共享模式(CountDownLatch)
│       └── Condition条件队列
├── 线程池
│   ├── 核心参数:corePoolSize、maximumPoolSize、workQueue
│   ├── 执行流程
│   └── 拒绝策略
└── 并发工具
    ├── 原子类:AtomicInteger、LongAdder
    ├── 同步工具:CountDownLatch、CyclicBarrier、Semaphore
    ├── 并发容器:ConcurrentHashMap、CopyOnWriteArrayList
    └── 异步编程:CompletableFuture

七、面试Tips

加分话术

  1. 底层结合:"volatile底层是通过在汇编层面添加lock前缀指令实现的,这个指令会触发缓存一致性协议..."
  2. 源码引用:"从AQS源码可以看到,获取锁失败时会通过shouldParkAfterFailedAcquire判断是否需要阻塞..."
  3. 实战经验:"之前优化过一个高并发系统,发现synchronized锁竞争激烈,通过锁升级分析发现是锁粒度太大..."

常见追问准备

  • 什么是指令重排序?volatile如何禁止?
  • synchronizedReentrantLock的区别?
  • AQS为什么用双向链表?
  • 线程池的核心线程数如何计算?
  • ThreadLocal的原理和内存泄漏问题?

避坑指南

  • ❌ 不要混淆“可见性”和“原子性”
  • ❌ 不要认为volatile能保证count++的原子性
  • ❌ 不要忽略线程池的优雅关闭
  • ✅ 强调锁升级的意义(减少重量级锁的开销)
  • ✅ 区分AQS的独占模式和共享模式

理解和掌握这些并发编程的核心知识,不仅是应对面试的关键,更是构建高性能、高可用Java应用的基石。如果你对Java并发或JVM底层有更深入的兴趣,欢迎访问云栈社区与其他开发者交流探讨。




上一篇:解构创造力:Margaret Boden分层理论、AI实现与语言模型的边界思考
下一篇:C#+YOLOv26+ONNX+Halcon混合架构下的7个性能陷阱与优化
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-3-26 04:45 , Processed in 0.809279 second(s), 41 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表