每日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方法的核心流程:
- 调用tryAcquire尝试获取资源(由子类实现)
- 获取失败则调用addWaiter将当前线程封装为Node节点加入CLH队列尾部
- 调用acquireQueued在队列中自旋等待获取资源
- 若等待过程中被中断,则调用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 线程池执行流程
- 提交任务时,若当前线程数小于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与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需要建立清晰的知识体系:
- 底层基础:CAS + volatile → Unsafe类 → AQS框架
- 锁机制:AQS → ReentrantLock → ReadWriteLock → StampedLock
- 同步工具:AQS → CountDownLatch / Semaphore / CyclicBarrier
- 并发容器:CAS + synchronized → ConcurrentHashMap / CopyOnWriteArrayList
- 线程池:ThreadPoolExecutor → 执行流程 → 拒绝策略 → 参数调优
- 原子类:CAS → AtomicInteger → LongAdder
面试中建议从AQS出发,串联各个知识点,展现对JUC整体架构的深入理解,而不是零散地回答每个问题。
感谢读者观看