每日Java面试场景题知识点之-JUC并发编程核心原理与实战一、JUC包全景概览JUCjava.util.concurrent是Java并发编程的核心工具包自JDK5引入以来经历了多次迭代优化目前已成为Java企业级开发中处理并发场景的基石。JUC包主要包含以下几大核心模块锁框架ReentrantLock、ReentrantReadWriteLock、StampedLock同步工具CountDownLatch、CyclicBarrier、Semaphore、Phaser原子类AtomicInteger、AtomicLong、AtomicReference等并发集合ConcurrentHashMap、CopyOnWriteArrayList、ConcurrentLinkedQueue线程池ThreadPoolExecutor、ScheduledThreadPoolExecutor、ForkJoinPool核心基础AQSAbstractQueuedSynchronizer二、AQS——JUC的灵魂框架2.1 AQS是什么AQSAbstractQueuedSynchronizer是JUC并发包的基石ReentrantLock、CountDownLatch、Semaphore、线程池等核心工具均基于AQS实现。面试中几乎必问AQS原理理解AQS是掌握JUC的第一步。AQS核心设计包含三个关键要素state同步状态使用volatile修饰的int类型变量通过CAS操作保证原子性修改实现了可见性、原子性和有序性三大特性CLH双向等待队列采用CLH锁队列变体每个节点保存等待线程、等待状态和前驱/后继节点引用用于管理获取资源失败的线程排队Condition单向链表内部类ConditionObject对标synchronized中的等待池线程执行await方法后封装为Node对象进入Condition链表等待唤醒2.2 AQS独占模式获取资源源码解析// 独占模式获取资源 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方法的核心流程调用tryAcquire尝试获取资源由子类实现获取失败则调用addWaiter将当前线程封装为Node节点加入CLH队列尾部调用acquireQueued在队列中自旋等待获取资源若等待过程中被中断则调用selfInterrupt恢复中断标志2.3 面试高频AQS唤醒线程为什么从后往前遍历这是面试中的经典问题。当持有资源的线程执行完成后需要从AQS双向链表中取出一个节点唤醒。正常情况下从head的next节点取即可但如果head的next节点已经取消waitStatusCANCELLED由于双向链表中节点入队时是先设置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 锁的可重入性实现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支持公平锁、可中断获取锁、超时获取锁、多条件变量Conditionsynchronized功能相对简单锁释放synchronized自动释放锁ReentrantLock必须在finally中手动unlock性能JDK6以后synchronized经过偏向锁、轻量级锁等优化性能已与ReentrantLock接近选择建议简单同步场景优先使用synchronized需要高级功能公平锁、可中断、超时、多条件时选择ReentrantLock四、线程池核心原理4.1 线程池7大核心参数public ThreadPoolExecutor( int corePoolSize, // 核心线程数 int maximumPoolSize, // 最大线程数 long keepAliveTime, // 非核心线程空闲存活时间 TimeUnit unit, // 存活时间单位 BlockingQueueRunnable workQueue, // 阻塞队列 ThreadFactory threadFactory, // 线程工厂 RejectedExecutionHandler handler // 拒绝策略 )面试中必须能完整说出这7个参数及其含义并解释执行流程。4.2 线程池执行流程提交任务时若当前线程数小于corePoolSize直接创建核心线程执行任务若核心线程数已满将任务放入阻塞队列workQueue若阻塞队列已满且当前线程数小于maximumPoolSize创建非核心线程执行任务若阻塞队列已满且线程数达到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与Semaphore6.1 CountDownLatchCountDownLatch是一个同步计数器允许一个或多个线程等待其他线程完成操作。核心方法countDown()计数器减1await()阻塞等待计数器归零典型应用场景主线程等待多个子线程全部完成后继续执行如批量数据导入、并行任务汇总等。底层原理基于AQS的共享模式state初始化为计数器值每次countDown将state减1await时如果state不为0则进入AQS队列等待。6.2 SemaphoreSemaphore信号量用于控制同时访问某个资源的线程数量本质是一个计数信号量。核心方法acquire()获取一个许可计数器减1若计数器为0则阻塞release()释放一个许可计数器加1典型应用场景限流如数据库连接池限制最大连接数、接口限流等。底层原理同样基于AQS共享模式实现state表示可用许可数。七、实战场景题精讲场景1如何实现一个线程安全的计数器根据场景选择不同方案单线程计数使用普通int或long低并发计数使用AtomicLongCAS无锁操作高并发计数使用LongAdder内部维护Cell数组不同线程对不同Cell进行CAS操作最终求和极大减少竞争// 高并发场景推荐 LongAdder counter new LongAdder(); counter.increment(); long result counter.sum();场景2如何控制某接口最多同时被5个线程访问Semaphore semaphore new Semaphore(5); public void apiMethod() { try { semaphore.acquire(); // 业务逻辑 } catch (InterruptedException e) { Thread.currentThread().interrupt(); } finally { semaphore.release(); } }场景3如何等待多个微服务全部就绪后才开始业务流程CountDownLatch latch new CountDownLatch(serviceCount); for (MicroService service : services) { new Thread(() - { service.init(); latch.countDown(); }).start(); } latch.await(); // 所有服务就绪开始业务流程八、JUC知识体系总结掌握JUC需要建立清晰的知识体系底层基础CAS volatile → Unsafe类 → AQS框架锁机制AQS → ReentrantLock → ReadWriteLock → StampedLock同步工具AQS → CountDownLatch / Semaphore / CyclicBarrier并发容器CAS synchronized → ConcurrentHashMap / CopyOnWriteArrayList线程池ThreadPoolExecutor → 执行流程 → 拒绝策略 → 参数调优原子类CAS → AtomicInteger → LongAdder面试中建议从AQS出发串联各个知识点展现对JUC整体架构的深入理解而不是零散地回答每个问题。感谢读者观看
每日Java面试场景题知识点之-JUC并发编程核心原理与实战
每日Java面试场景题知识点之-JUC并发编程核心原理与实战一、JUC包全景概览JUCjava.util.concurrent是Java并发编程的核心工具包自JDK5引入以来经历了多次迭代优化目前已成为Java企业级开发中处理并发场景的基石。JUC包主要包含以下几大核心模块锁框架ReentrantLock、ReentrantReadWriteLock、StampedLock同步工具CountDownLatch、CyclicBarrier、Semaphore、Phaser原子类AtomicInteger、AtomicLong、AtomicReference等并发集合ConcurrentHashMap、CopyOnWriteArrayList、ConcurrentLinkedQueue线程池ThreadPoolExecutor、ScheduledThreadPoolExecutor、ForkJoinPool核心基础AQSAbstractQueuedSynchronizer二、AQS——JUC的灵魂框架2.1 AQS是什么AQSAbstractQueuedSynchronizer是JUC并发包的基石ReentrantLock、CountDownLatch、Semaphore、线程池等核心工具均基于AQS实现。面试中几乎必问AQS原理理解AQS是掌握JUC的第一步。AQS核心设计包含三个关键要素state同步状态使用volatile修饰的int类型变量通过CAS操作保证原子性修改实现了可见性、原子性和有序性三大特性CLH双向等待队列采用CLH锁队列变体每个节点保存等待线程、等待状态和前驱/后继节点引用用于管理获取资源失败的线程排队Condition单向链表内部类ConditionObject对标synchronized中的等待池线程执行await方法后封装为Node对象进入Condition链表等待唤醒2.2 AQS独占模式获取资源源码解析// 独占模式获取资源 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方法的核心流程调用tryAcquire尝试获取资源由子类实现获取失败则调用addWaiter将当前线程封装为Node节点加入CLH队列尾部调用acquireQueued在队列中自旋等待获取资源若等待过程中被中断则调用selfInterrupt恢复中断标志2.3 面试高频AQS唤醒线程为什么从后往前遍历这是面试中的经典问题。当持有资源的线程执行完成后需要从AQS双向链表中取出一个节点唤醒。正常情况下从head的next节点取即可但如果head的next节点已经取消waitStatusCANCELLED由于双向链表中节点入队时是先设置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 锁的可重入性实现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支持公平锁、可中断获取锁、超时获取锁、多条件变量Conditionsynchronized功能相对简单锁释放synchronized自动释放锁ReentrantLock必须在finally中手动unlock性能JDK6以后synchronized经过偏向锁、轻量级锁等优化性能已与ReentrantLock接近选择建议简单同步场景优先使用synchronized需要高级功能公平锁、可中断、超时、多条件时选择ReentrantLock四、线程池核心原理4.1 线程池7大核心参数public ThreadPoolExecutor( int corePoolSize, // 核心线程数 int maximumPoolSize, // 最大线程数 long keepAliveTime, // 非核心线程空闲存活时间 TimeUnit unit, // 存活时间单位 BlockingQueueRunnable workQueue, // 阻塞队列 ThreadFactory threadFactory, // 线程工厂 RejectedExecutionHandler handler // 拒绝策略 )面试中必须能完整说出这7个参数及其含义并解释执行流程。4.2 线程池执行流程提交任务时若当前线程数小于corePoolSize直接创建核心线程执行任务若核心线程数已满将任务放入阻塞队列workQueue若阻塞队列已满且当前线程数小于maximumPoolSize创建非核心线程执行任务若阻塞队列已满且线程数达到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与Semaphore6.1 CountDownLatchCountDownLatch是一个同步计数器允许一个或多个线程等待其他线程完成操作。核心方法countDown()计数器减1await()阻塞等待计数器归零典型应用场景主线程等待多个子线程全部完成后继续执行如批量数据导入、并行任务汇总等。底层原理基于AQS的共享模式state初始化为计数器值每次countDown将state减1await时如果state不为0则进入AQS队列等待。6.2 SemaphoreSemaphore信号量用于控制同时访问某个资源的线程数量本质是一个计数信号量。核心方法acquire()获取一个许可计数器减1若计数器为0则阻塞release()释放一个许可计数器加1典型应用场景限流如数据库连接池限制最大连接数、接口限流等。底层原理同样基于AQS共享模式实现state表示可用许可数。七、实战场景题精讲场景1如何实现一个线程安全的计数器根据场景选择不同方案单线程计数使用普通int或long低并发计数使用AtomicLongCAS无锁操作高并发计数使用LongAdder内部维护Cell数组不同线程对不同Cell进行CAS操作最终求和极大减少竞争// 高并发场景推荐 LongAdder counter new LongAdder(); counter.increment(); long result counter.sum();场景2如何控制某接口最多同时被5个线程访问Semaphore semaphore new Semaphore(5); public void apiMethod() { try { semaphore.acquire(); // 业务逻辑 } catch (InterruptedException e) { Thread.currentThread().interrupt(); } finally { semaphore.release(); } }场景3如何等待多个微服务全部就绪后才开始业务流程CountDownLatch latch new CountDownLatch(serviceCount); for (MicroService service : services) { new Thread(() - { service.init(); latch.countDown(); }).start(); } latch.await(); // 所有服务就绪开始业务流程八、JUC知识体系总结掌握JUC需要建立清晰的知识体系底层基础CAS volatile → Unsafe类 → AQS框架锁机制AQS → ReentrantLock → ReadWriteLock → StampedLock同步工具AQS → CountDownLatch / Semaphore / CyclicBarrier并发容器CAS synchronized → ConcurrentHashMap / CopyOnWriteArrayList线程池ThreadPoolExecutor → 执行流程 → 拒绝策略 → 参数调优原子类CAS → AtomicInteger → LongAdder面试中建议从AQS出发串联各个知识点展现对JUC整体架构的深入理解而不是零散地回答每个问题。感谢读者观看