深入理解 Java 并发:CAS、AQS、ReentrantLock 与线程池
在 Java 并发编程中,CAS(Compare-And-Swap)、AQS(AbstractQueuedSynchronizer)、ReentrantLock 以及线程池是绕不开的核心话题。本文将基于面试常见问题,逐一剖析这些概念的原理、应用场景以及注意事项。
1. CAS 和 AQS 是什么?
CAS(Compare-And-Swap)
CAS 是一种无锁的原子操作,核心思想是通过比较和交换来更新变量值。它依赖硬件级别的指令(如 CPU 的 cmpxchg
),通常包含三个参数:
- 内存位置(V):要更新的变量。
- 旧值(A):预期值。
- 新值(B):要设置的值。
执行逻辑是:如果内存位置 V 的当前值等于旧值 A,则将 V 更新为新值 B;否则不做任何操作。CAS 的典型实现见 Java 的 AtomicInteger
类,例如 compareAndSet
方法。
AQS(AbstractQueuedSynchronizer)
AQS 是 Java 并发包(java.util.concurrent
)的核心框架,用于构建锁和同步器。它通过一个基于 FIFO 队列的线程等待机制和一个 state
状态变量来管理线程同步。AQS 是许多同步工具(如 ReentrantLock、CountDownLatch、Semaphore)的基石。
两者的关系:
- CAS 是 AQS 的底层实现手段之一。AQS 通过 CAS 操作
state
字段来实现线程的竞争和状态更新。 - CAS 注重无锁操作的高效性,而 AQS 提供了一个通用的同步框架。
2. ReentrantLock 是公平锁还是非公平锁?
ReentrantLock
默认是非公平锁,但可以通过构造参数设置为公平锁:
java
ReentrantLock lock = new ReentrantLock(); // 默认非公平锁
ReentrantLock fairLock = new ReentrantLock(true); // 公平锁
- 非公平锁:新来的线程可能抢占刚释放的锁,吞吐量更高,但可能导致某些线程长时间等待。
- 公平锁:严格按照线程请求锁的顺序分配,减少饥饿现象,但性能稍低(因为需要维护队列)。
底层实现上,ReentrantLock 基于 AQS,通过 state
表示锁的重入次数,非公平模式下会优先尝试 CAS 获取锁,而公平模式会检查等待队列。
3. 线程池是什么?
线程池是一种线程管理机制,通过复用线程来减少创建和销毁线程的开销。Java 的 ThreadPoolExecutor
是线程池的核心实现,构造参数包括:
- 核心线程数(corePoolSize):常驻线程数。
- 最大线程数(maximumPoolSize):允许的最大线程数。
- 空闲线程存活时间(keepAliveTime):超出核心线程数的线程空闲多久后销毁。
- 任务队列(workQueue):存储待执行任务的队列。
- 拒绝策略(RejectedExecutionHandler):任务超载时的处理方式。
常见用法:
java
ExecutorService executor = new ThreadPoolExecutor(
2, 5, 60L, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(10),
new ThreadPoolExecutor.AbortPolicy()
);
executor.submit(() -> System.out.println("Task executed"));
线程池的优势在于资源复用、任务排队和并发控制,适用于高并发场景。
4. CAS 如果失败会发生什么?
CAS 失败意味着当前线程检测到内存值与预期值不一致,通常有以下处理方式:
-
自旋重试 :线程循环调用 CAS,直到成功。例如
AtomicInteger
的incrementAndGet
方法:javado { oldValue = get(); } while (!compareAndSet(oldValue, oldValue + 1));
-
放弃或回退:根据业务逻辑选择放弃操作或执行其他策略。
-
ABA 问题 :CAS 失败可能隐藏 ABA 问题(值从 A 变为 B 又变回 A),可以通过版本号(如
AtomicStampedReference
)解决。
失败的原因通常是多线程竞争导致的并发修改。
5. CAS 和 AQS 基于什么场景使用?
-
CAS 适用场景:
- 单变量的原子更新,例如计数器(
AtomicInteger
)、标志位。 - 轻量级、无锁操作,要求高性能且竞争不激烈。
- 示例:乐观锁实现、简单的并发数据结构。
- 单变量的原子更新,例如计数器(
-
AQS 适用场景:
- 需要复杂的同步逻辑,如锁、信号量、倒计时器。
- 多线程协作或资源竞争,例如线程安全的队列、读写锁。
- 示例:
ReentrantLock
、Semaphore
、CountDownLatch
。
两者的选择取决于同步需求的复杂度和性能要求:CAS 简单高效,AQS 功能强大但稍重。
6. 为什么 AQS 适用于写的场景?
AQS 特别适合"写"场景的原因在于:
- 状态管理 :AQS 的
state
字段可以表示锁状态,通过 CAS 更新实现线程安全,天然适合写操作的竞争。 - 队列机制:写操作通常需要互斥,AQS 的 FIFO 队列确保线程按序获取资源,避免无序竞争。
- 灵活性:AQS 支持独占模式(如 ReentrantLock)和共享模式(如 Semaphore),能适应多种写场景。
- 重入支持 :写操作常涉及递归或嵌套调用,AQS 的重入机制(通过
state
计数)非常契合。
例如,ReentrantLock
在写多读少的场景中,通过 AQS 保证写操作的线程安全和高吞吐量。
总结
- CAS 是无锁并发的基础,高效但适用范围有限。
- AQS 是构建复杂同步器的利器,适合需要队列管理和状态控制的场景。
- ReentrantLock 默认非公平,灵活性强。
- 线程池 是并发任务管理的核心工具。
理解这些概念的原理和适用场景,不仅能应对面试,更能在实际开发中设计高效的并发系统。希望这篇博客对你的学习有所帮助!