自定义同步器案例深度剖析——AQS框架下的实战指南
目录导读
- 为什么需要自定义同步器?——从痛点出发
- 基石:AQS(AbstractQueuedSynchronizer)核心原理速览
- 案例实战:实现一个不可重入的共享锁(NonReentrantSharedLock)
- 细节打磨:同步器的状态管理、等待队列与中断响应
- 测试与验证:并发环境下的正确性证明
- 常见陷阱与性能优化建议
- 问答环节:解决你关于自定义同步器的5个高频疑问
为什么需要自定义同步器?
Java内置的synchronized、ReentrantLock、Semaphore等工具能覆盖90%的业务场景,但当你需要独特的语义——同一时刻仅允许N个线程访问某个资源,且线程不可重入”时,内置工具要么性能不佳,要么语义不符,基于java.util.concurrent.locks.AbstractQueuedSynchronizer(AQS)自定义同步器,就成了优雅且高效的解决方案,AQS提供了状态管理、线程阻塞/唤醒、排队机制的完整框架,你只需实现几个钩子方法。

基石:AQS核心原理速览
AQS内部维护一个volatile int state(同步状态)和一个CLH变体的FIFO等待队列,三个关键方法你需覆写:
tryAcquire(int arg):独占式获取锁(返回boolean)tryRelease(int arg):独占式释放锁tryAcquireShared(int arg):共享式获取锁(返回int,负数表示失败,0表示成功但无剩余资源,正数表示成功且有余量)tryReleaseShared(int arg):共享式释放锁
框架通过acquire/release模板方法调用这些钩子,自动处理排队、park/unpark线程。
案例实战:实现一个不可重入的共享锁(NonReentrantSharedLock)
业务需求:最多允许3个线程同时持有锁,同一线程多次获取会失败(不重入)。
public class NonReentrantSharedLock {
private final Sync sync = new Sync(3); // 最大并发3
private static final class Sync extends AbstractQueuedSynchronizer {
Sync(int maxCount) { setState(maxCount); }
@Override
protected int tryAcquireShared(int arg) {
while (true) {
int current = getState();
int remain = current - arg;
if (remain < 0 || compareAndSetState(current, remain)) {
return remain; // 负数代表获取失败
}
}
}
@Override
protected boolean tryReleaseShared(int arg) {
while (true) {
int current = getState();
int next = current + arg;
if (compareAndSetState(current, next)) {
return true;
}
}
}
}
public void lock() { sync.acquireShared(1); }
public void unlock() { sync.releaseShared(1); }
}
关键点:tryAcquireShared返回剩余许可数,如果为负则AQS会将该线程包装成Node放入等待队列并挂起,因为tryAcquireShared不检查重入,所以天然实现“不可重入”——第二次调用时current已减少,可能返回负数。
细节打磨:状态管理、中断与超时
- 中断响应:调用
acquireSharedInterruptibly替代acquireShared,在等待期间响应中断。 - 超时控制:使用
tryAcquireSharedNanos重载版本。 - 公平性:AQS默认非公平,若想公平排队,可在
tryAcquireShared中调用hasQueuedPredecessors()判断是否有前驱节点。 - 状态值语义:本例state表示“剩余许可数”,初始为3,释放时递增,获取时递减。
测试与验证
使用CountDownLatch创建3个线程同时争抢,每个线程循环获取/释放10000次,断言最大并发不超过3,还可借助jstack查看线程状态是否正常BLOCKED/WAITING。
常见陷阱与性能优化
- 陷阱1:忘记使用CAS循环,导致并发下状态错乱。
- 陷阱2:
tryReleaseShared返回false时不会唤醒后继线程,需确保释放成功且状态变化后返回true。 - 优化:对于超高竞争场景,可考虑
AbstractQueuedSynchronizer的setHeadAndPropagate传播机制,减少唤醒抖动。
问答环节
Q1:AQS和synchronized的最大区别是什么? A1:AQS基于CAS+volatile,提供灵活的锁语义(共享/独占、公平/非公平),且支持超时和中断,而synchronized是JVM内置的偏向锁/轻量级锁,不可手动控制队列策略。
Q2:我的同步器是否需要覆盖所有四个钩子方法?
A2:不需要,独占锁只需tryAcquire/tryRelease;共享锁只需tryAcquireShared/tryReleaseShared。
Q3:state初始值如何设计? A3:完全自定义,可表示“许可数”(Semaphore)、"资源占用状态"(ReentrantLock的0/1)、甚至“读写状态分离”。
Q4:为什么我的自定义锁性能比ReentrantLock差?
A4:可能是你在钩子方法中做了耗时操作,或错误地使用了yield而不是依赖AQS的park/unpark,保证CAS自旋次数少,且快速失败。
Q5:如何保证内存可见性?
A5:AQS的state是volatile的,且通过CAS和LockSupport的park/unpark(内部有内存屏障)保证跨线程状态的可见性和有序性,你无需额外加锁。
自定义同步器并非“造轮子”,而是在特殊并发控制场景下的“定制引擎”,通过AQS的模板方法模式,你将繁琐的队列管理交给专家,只需定义状态与获取/释放策略,先画清状态机,再实现钩子,最后用并发测试验证,这样,你不仅能写出高性能的同步器,更能深入理解JUC的底层哲学。