阿里P7并发专项面经:CountDownLatch、CyclicBarrier、Semaphore、ConcurrentHashMap

30 阅读6分钟

上篇设计模式聊完,这篇进入Java并发专项。第28篇P7高级讲过AQS基础和线程池参数,这篇往深处走——并发工具类原理、ConcurrentHashMap底层、线程池实战调优。

P7并发面试两个层次:背概念(CountDownLatch干什么用的)和讲原理(CountDownLatch内部基于AQS共享模式,state是计数值)。P7 offer要第二层。

今天8道题覆盖P7并发深层考点,每道给出原理级答案。

Q1:CountDownLatch原理?怎么用?

作用:让一个或多个线程等待一组事件完成。典型场景:主线程等待N个子任务执行完再汇总结果。

原理:基于AQS共享模式。state初始值为count,每个countDown()releaseShared把state减1(CAS操作),减到0时AQS释放所有等待线程。await()acquireSharedInterruptibly,state>0就park阻塞,state=0时unpark。

用法new CountDownLatch(3),子任务完成后countDown(),主线程await()等待。等3个countDown都执行完主线程继续。

追问:CountDownLatch能重复使用吗?不能。count到0后废了,再await直接返回。需要重复等待用CyclicBarrier。

Q2:CyclicBarrier和CountDownLatch区别?

CountDownLatch:一次性使用,等N个事件发生。一个线程等,多个线程触发countDown。

CyclicBarrier:可重复使用,等N个线程互相等待。N个线程都到barrier后一起继续。适合多线程协作场景(如并行计算汇总)。

CyclicBarrier原理:内部用ReentrantLock + Condition。每个线程await()获取锁,generation计数器+1,不是末位就condition.await阻塞。末位线程到时signalAll唤醒所有线程,generation+1进入下一轮。

追问:CyclicBarrier的broken状态怎么回事?如果某个等待线程被中断或超时,barrier进入broken状态,其他等待线程抛BrokenBarrierException。可以reset()重置。

Q3:Semaphore原理?Android怎么用?

作用:控制同时访问资源的线程数。比如限流——同时最多允许3个线程访问某资源。

原理:基于AQS共享模式。state初始值为许可数permits。acquire()获取许可(state减1),state=0时阻塞。release()归还许可(state加1),唤醒等待线程。

Android场景:线程池任务限流。比如图片下载最多同时3个任务——Semaphore(3),每个下载任务先acquire完成时release。超过3个的任务排队等。

追问:Semaphore和synchronized什么区别?synchronized是互斥锁(同一时刻一个线程),Semaphore是计数锁(同一时刻N个线程)。Semaphore(1)效果类似synchronized但不支持可重入。

Q4:ReadWriteLock原理?什么时候用?

作用:读多写少场景提升并发。读锁共享(多线程可同时读),写锁独占(写时其他线程阻塞)。

原理:ReentrantReadWriteLock内部维护两个锁。AQS的state高16位存读锁计数,低16位存写锁计数。读锁acquire时检查写锁是否被占用——没被占用就CAS增加读计数。写锁acquire时检查读锁和写锁——都没有才获取。

锁降级:持有写锁 → 获取读锁 → 释放写锁。写锁降级为读锁,中间数据对其他读线程可见。锁升级(读→写)不支持——可能导致死锁。

Android场景:配置管理器——多个线程读配置(读锁),后台更新配置(写锁)。读多写少场景性能提升明显。

Q5:ConcurrentHashMap原理?JDK 7和8区别?

JDK 7:分段锁(Segment)。ConcurrentHashMap由16个Segment组成,每个Segment是一个HashMap + ReentrantLock。put时根据hash定位到Segment,锁住该Segment操作。并发度=Segment数量(默认16)。

JDK 8:抛弃Segment,用CAS + synchronized锁单个桶(Node)。put时hash定位到桶,桶为空就CAS插入;桶非空就synchronized锁住桶头节点,遍历链表/红黑树插入。粒度更细,并发度=桶数量。

JDK 8红黑树:链表长度超过8且数组长度超过64时,链表转红黑树(查找从O(n)变O(log n))。低于6时退化回链表。

追问:size()怎么做到线程安全?JDK 8用baseCount + CounterCell数组(类似LongAdder)。每个线程CAS更新不同CounterCell,size时求和。避免所有线程竞争同一个计数器。

Q6:ConcurrentHashMap的put流程?

JDK 8的put流程:计算hash定位桶 → 桶空CAS插入 → 桶非空synchronized锁桶头 → 链表遍历更新或红黑树插入 → 链表长度>8转红黑树 → 容量超threshold扩容(多线程协助迁移)。

追问:扩容怎么多线程?sizeCtl记录状态,扩容时<0。其他线程检测到帮忙迁移——每个线程分配一部分桶(ForwardingNode标记已迁移)。迁移完更新sizeCtl。

Q7:线程池实战怎么调优?

监控指标:activeCount(活跃线程数)、queueSize(队列积压量)、completedTaskCount(完成任务数)、rejectedCount(拒绝任务数)。自定义ThreadPoolExecutor重写afterExecute统计这些指标。

动态调参:生产环境线程池参数需要动态调整。阿里开源的DynamicTp方案:参数存配置中心(Apollo/Nacos),修改后推送到客户端,调setCorePoolSize/setMaximumPoolSize实时生效。

Android实践:全局线程池一个就够。推荐配置——corePoolSize=CPU核心数,maximumPoolSize=CPU核心数*2,keepAliveTime=60s,队列用LinkedBlockingQueue(128)。不要用Executors的固定大小线程池(无界队列有OOM风险)。

追问:线程池shutdown和shutdownNow区别?shutdown等队列任务执行完再关闭(不接受新任务)。shutdownNow立即中断所有线程返回未执行的任务列表。优雅关闭用shutdown+awaitTermination(timeout)。

Q8:CompletableFuture怎么用?跟Future什么区别?

Future的问题:get()阻塞等待结果,不能链式组合多个异步任务。多个Future组合要手动管依赖。

CompletableFuture:支持链式组合。supplyAsync创建异步任务,thenApply转换结果,thenCompose串联下一个异步任务,thenCombine合并两个结果,allOf等全部完成。链式写法比回调嵌套清晰得多。

Android场景:启动时多个初始化任务并行(网络请求+数据库读取+文件IO),allOf等全部完成再进首页。比CountDownLatch写法更简洁,支持链式处理结果。


面试Tips:P7并发专项面试CountDownLatch和CyclicBarrier要能讲清AQS共享模式原理。Semaphore要能讲许可计数机制。ReadWriteLock考锁降级。ConcurrentHashMap考JDK 8的CAS+synchronized和红黑树转换。线程池考动态调参和监控指标。CompletableFuture考链式组合。面试前刷一遍java.util.concurrent包的源码。

下一篇进入JVM专项——GC算法深入、G1/ZGC原理、OOM排查实战、MAT内存分析。


用过CountDownLatch的同学评论区说说,你项目里用来解决什么问题?

本系列连载中,关注不迷路,下一篇:阿里P7高级Android(JVM专项)面试真题

系列简介:Android大厂面经连载,覆盖字节跳动、腾讯、阿里、美团等40+企业,从初级到架构师全岗位覆盖。每篇文章包含真实面试题+详细答案+代码示例,帮你拿到大厂Offer。