每日Java面试场景题知识点之-JUC并发编程核心原理与实战

每日Java面试场景题知识点之-JUC并发编程核心原理与实战

一、JUC包全景概览

JUC(java.util.concurrent)是Java并发编程的核心工具包,自JDK5引入以来,经历了多次迭代优化,目前已成为Java企业级开发中处理并发场景的基石。JUC包主要包含以下几大核心模块:

  • 锁框架:ReentrantLock、ReentrantReadWriteLock、StampedLock
  • 同步工具:CountDownLatch、CyclicBarrier、Semaphore、Phaser
  • 原子类:AtomicInteger、AtomicLong、AtomicReference等
  • 并发集合:ConcurrentHashMap、CopyOnWriteArrayList、ConcurrentLinkedQueue
  • 线程池:ThreadPoolExecutor、ScheduledThreadPoolExecutor、ForkJoinPool
  • 核心基础:AQS(AbstractQueuedSynchronizer)

二、AQS------JUC的灵魂框架

2.1 AQS是什么?

AQS(AbstractQueuedSynchronizer)是JUC并发包的基石,ReentrantLock、CountDownLatch、Semaphore、线程池等核心工具均基于AQS实现。面试中几乎必问AQS原理,理解AQS是掌握JUC的第一步。

AQS核心设计包含三个关键要素:

  • state同步状态:使用volatile修饰的int类型变量,通过CAS操作保证原子性修改,实现了可见性、原子性和有序性三大特性
  • CLH双向等待队列:采用CLH锁队列变体,每个节点保存等待线程、等待状态和前驱/后继节点引用,用于管理获取资源失败的线程排队
  • Condition单向链表:内部类ConditionObject对标synchronized中的等待池,线程执行await方法后封装为Node对象进入Condition链表等待唤醒

2.2 AQS独占模式获取资源源码解析

java 复制代码
// 独占模式获取资源
public final void acquire(int arg) {
    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;
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    enq(node);
    return node;
}

acquire方法的核心流程:

  1. 调用tryAcquire尝试获取资源(由子类实现)
  2. 获取失败则调用addWaiter将当前线程封装为Node节点加入CLH队列尾部
  3. 调用acquireQueued在队列中自旋等待获取资源
  4. 若等待过程中被中断,则调用selfInterrupt恢复中断标志

2.3 面试高频:AQS唤醒线程为什么从后往前遍历?

这是面试中的经典问题。当持有资源的线程执行完成后,需要从AQS双向链表中取出一个节点唤醒。正常情况下从head的next节点取即可,但如果head的next节点已经取消(waitStatus=CANCELLED),由于双向链表中节点入队时是先设置prev再CAS设置tail,最后才设置前驱节点的next,如果从前往后遍历,可能会遇到next指针还未设置完成的情况,导致节点丢失。而从后往前遍历通过prev指针一定能够找到所有有效节点,因为prev指针在入队时是率先设置好的。

2.4 AQS为什么用双向链表而不用单向链表?

  • 取消节点处理:当队列中的节点取消时,双向链表可以O(1)完成节点摘除,单向链表需要O(n)遍历
  • 唤醒传播:双向链表支持从前向后传播唤醒信号,也支持从后向前遍历查找有效节点
  • 阻塞检查:每个节点需要检查其前驱节点状态来决定是否阻塞自己,双向链表天然支持这种前驱查询

三、ReentrantLock实现原理

3.1 公平锁与非公平锁

ReentrantLock基于AQS实现,支持公平锁和非公平锁两种模式:

非公平锁(默认模式):

  • 线程获取锁时直接尝试CAS修改state,失败后才进入队列
  • 可能产生"插队"现象,但减少了线程切换开销,吞吐量更高
  • 适用于大部分业务场景

公平锁

  • 严格按照FIFO顺序获取锁
  • hasQueuedPredecessors()方法检查是否有更早的等待线程
  • 避免了线程饥饿,但性能略低于非公平锁
  • 适用于对公平性要求严格的场景

3.2 锁的可重入性实现

java 复制代码
final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
        if (compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    } else if (current == getExclusiveOwnerThread()) {
        int nextc = c + acquires;
        if (nextc < 0)
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    return false;
}

可重入的核心逻辑:当state不为0时,判断当前线程是否为持有锁的线程(通过getExclusiveOwnerThread判断),如果是,则将state值累加,实现重入计数。释放锁时每次将state减1,减到0时才真正释放锁。

3.3 面试场景题:synchronized与ReentrantLock如何选择?

面试官常问二者区别,建议从以下维度作答:

  • 实现层面:synchronized是JVM层面的关键字,ReentrantLock是API层面的类
  • 功能丰富度:ReentrantLock支持公平锁、可中断获取锁、超时获取锁、多条件变量(Condition),synchronized功能相对简单
  • 锁释放:synchronized自动释放锁,ReentrantLock必须在finally中手动unlock
  • 性能:JDK6以后synchronized经过偏向锁、轻量级锁等优化,性能已与ReentrantLock接近
  • 选择建议:简单同步场景优先使用synchronized;需要高级功能(公平锁、可中断、超时、多条件)时选择ReentrantLock

四、线程池核心原理

4.1 线程池7大核心参数

java 复制代码
public ThreadPoolExecutor(
    int corePoolSize,        // 核心线程数
    int maximumPoolSize,     // 最大线程数
    long keepAliveTime,      // 非核心线程空闲存活时间
    TimeUnit unit,           // 存活时间单位
    BlockingQueue<Runnable> workQueue,  // 阻塞队列
    ThreadFactory threadFactory,        // 线程工厂
    RejectedExecutionHandler handler    // 拒绝策略
)

面试中必须能完整说出这7个参数及其含义,并解释执行流程。

4.2 线程池执行流程

  1. 提交任务时,若当前线程数小于corePoolSize,直接创建核心线程执行任务
  2. 若核心线程数已满,将任务放入阻塞队列workQueue
  3. 若阻塞队列已满,且当前线程数小于maximumPoolSize,创建非核心线程执行任务
  4. 若阻塞队列已满且线程数达到maximumPoolSize,执行拒绝策略

4.3 四大拒绝策略

  • AbortPolicy(默认):抛出RejectedExecutionException异常
  • CallerRunsPolicy:由提交任务的线程自己执行该任务
  • DiscardPolicy:直接丢弃任务,不抛异常
  • DiscardOldestPolicy:丢弃队列中最老的任务,然后重新提交当前任务

4.4 面试场景题:核心线程数如何设置?

这是面试高频场景题,核心思路是区分任务类型:

  • CPU密集型任务:核心线程数设置为 CPU核心数 + 1。+1是为了在某个线程偶尔因为页缺失故障等原因暂停时,额外的线程能保证CPU周期不被浪费
  • IO密集型任务:核心线程数设置为 CPU核心数 * 2 或 CPU核心数 / (1 - 阻塞系数)。IO密集型任务线程大部分时间在等待,需要更多线程来提高并发度
  • 混合型任务:根据实际业务中CPU和IO的比例进行折中,或根据压测结果动态调整

实际生产中,建议通过压测来确定最优线程数,不能纸上谈兵。

五、ConcurrentHashMap并发安全原理

5.1 HashMap为什么线程不安全?

  • JDK7:并发扩容时可能形成环形链表,导致死循环
  • JDK8:并发put时可能发生数据覆盖。两个线程同时判断hash槽位为null,后写入的值会覆盖先写入的值

5.2 ConcurrentHashMap如何保证线程安全?

JDK8的ConcurrentHashMap摒弃了JDK7的Segment分段锁设计,改用CAS + synchronized方案:

  • 初始化数组:通过CAS操作sizeCtl变量保证只有一个线程初始化数组
  • put操作:如果hash槽位为null,使用CAS写入;如果槽位不为null,对头节点加synchronized锁后再操作
  • 扩容:支持多线程协同扩容,每个线程负责一段桶的迁移,通过transferIndex分配迁移任务
  • 计数:使用LongAdder思想的CounterCell数组,减少CAS竞争,提高并发计数性能

5.3 面试高频:为什么链表长度到8转为红黑树?

源码注释中给出了详细解释:

  • 泊松分布计算表明,链表长度达到8的概率仅为0.00000006,属于极端情况
  • 红黑树节点占用空间是普通节点的2倍,只在必要时才转换
  • 链表长度降到6时退化回链表,中间差值7是为了避免频繁在链表和红黑树之间转换
  • 选择8作为阈值是时间和空间权衡的结果

5.4 ConcurrentHashMap的读操作会阻塞吗?

不会。ConcurrentHashMap的get操作不需要加锁:

  • Node的val和next使用volatile修饰,保证了内存可见性
  • 读操作直接读取volatile变量,无需加锁
  • 即使在扩容过程中,也能通过ForwardingNode访问到正确的数据

六、CountDownLatch与Semaphore

6.1 CountDownLatch

CountDownLatch是一个同步计数器,允许一个或多个线程等待其他线程完成操作。

核心方法:

  • countDown():计数器减1
  • await():阻塞等待计数器归零

典型应用场景:主线程等待多个子线程全部完成后继续执行,如批量数据导入、并行任务汇总等。

底层原理:基于AQS的共享模式,state初始化为计数器值,每次countDown将state减1,await时如果state不为0则进入AQS队列等待。

6.2 Semaphore

Semaphore(信号量)用于控制同时访问某个资源的线程数量,本质是一个计数信号量。

核心方法:

  • acquire():获取一个许可,计数器减1,若计数器为0则阻塞
  • release():释放一个许可,计数器加1

典型应用场景:限流,如数据库连接池限制最大连接数、接口限流等。

底层原理:同样基于AQS共享模式实现,state表示可用许可数。

七、实战场景题精讲

场景1:如何实现一个线程安全的计数器?

根据场景选择不同方案:

  • 单线程计数:使用普通int或long
  • 低并发计数:使用AtomicLong,CAS无锁操作
  • 高并发计数:使用LongAdder,内部维护Cell数组,不同线程对不同Cell进行CAS操作,最终求和,极大减少竞争
java 复制代码
// 高并发场景推荐
LongAdder counter = new LongAdder();
counter.increment();
long result = counter.sum();

场景2:如何控制某接口最多同时被5个线程访问?

java 复制代码
Semaphore semaphore = new Semaphore(5);

public void apiMethod() {
    try {
        semaphore.acquire();
        // 业务逻辑
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
    } finally {
        semaphore.release();
    }
}

场景3:如何等待多个微服务全部就绪后才开始业务流程?

java 复制代码
CountDownLatch latch = new CountDownLatch(serviceCount);

for (MicroService service : services) {
    new Thread(() -> {
        service.init();
        latch.countDown();
    }).start();
}

latch.await();
// 所有服务就绪,开始业务流程

八、JUC知识体系总结

掌握JUC需要建立清晰的知识体系:

  1. 底层基础:CAS + volatile → Unsafe类 → AQS框架
  2. 锁机制:AQS → ReentrantLock → ReadWriteLock → StampedLock
  3. 同步工具:AQS → CountDownLatch / Semaphore / CyclicBarrier
  4. 并发容器:CAS + synchronized → ConcurrentHashMap / CopyOnWriteArrayList
  5. 线程池:ThreadPoolExecutor → 执行流程 → 拒绝策略 → 参数调优
  6. 原子类:CAS → AtomicInteger → LongAdder

面试中建议从AQS出发,串联各个知识点,展现对JUC整体架构的深入理解,而不是零散地回答每个问题。

感谢读者观看

相关推荐
小张小张爱学习14 小时前
Java-io流
java·开发语言
林森i14 小时前
vscode设置java
java·ide·vscode
人道领域14 小时前
新项目该怎么入手?我用Claude code 接入小米mimo复盘黑马点评,看他的思路是什么。
java·人工智能·后端·mimo·claude code
AC赳赳老秦14 小时前
OpenClaw多Agent分工协作:按工作模块拆分Agent,实现全流程自动化闭环
java·大数据·数据库·python·自动化·php·openclaw
shjsjdmmd14 小时前
IntelliJ IDEA 接入 Claude API 完整教程:用 Continue 插件配置 Claude 编程助手
java·ide·intellij-idea
guslegend14 小时前
第2节:系统架构设计
java·spring boot·spring
索西引擎14 小时前
【LangChain 1.0】接入 DeepSeek API:从 API Key 申请到流式响应的完整实践
android·java·langchain
我是一颗柠檬14 小时前
【JDK8新特性】新工具类与API改进Day11
java·开发语言·后端·intellij-idea
牧瀬クリスだ14 小时前
多线程安全:从原子性到锁机制
java·开发语言