自定义同步器案例

wen java案例 1

自定义同步器案例深度剖析——AQS框架下的实战指南

目录导读

  1. 为什么需要自定义同步器?——从痛点出发
  2. 基石:AQS(AbstractQueuedSynchronizer)核心原理速览
  3. 案例实战:实现一个不可重入的共享锁(NonReentrantSharedLock)
  4. 细节打磨:同步器的状态管理、等待队列与中断响应
  5. 测试与验证:并发环境下的正确性证明
  6. 常见陷阱与性能优化建议
  7. 问答环节:解决你关于自定义同步器的5个高频疑问

为什么需要自定义同步器?

Java内置的synchronizedReentrantLockSemaphore等工具能覆盖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循环,导致并发下状态错乱。
  • 陷阱2tryReleaseShared返回false时不会唤醒后继线程,需确保释放成功且状态变化后返回true。
  • 优化:对于超高竞争场景,可考虑AbstractQueuedSynchronizersetHeadAndPropagate传播机制,减少唤醒抖动。

问答环节

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的底层哲学。

上一篇Java NIO案例

下一篇AQS原理案例

抱歉,评论功能暂时关闭!