面试必问:Semaphore 凭什么靠 AQS + CAS 实现限流?

0 阅读6分钟

一、实现原理

Semaphore 基于 AQS(AbstractQueuedSynchronizer) + CAS 实现,可通过构造方法传入布尔参数,选择公平锁非公平锁默认采用非公平锁,控制同一时间内访问共享资源的最大线程数

⚠️注意:
✔️acquire 方法用于向 Semaphore 申请获取许可证,该方法具有阻塞特性
✔️tryAcquire 方法用于尝试向 Semaphore 获取许可证,核心为非阻塞逻辑
✔️信号量赋值AbstractQueuedSynchronizer(AQS)的state属性值


二,使用场景

Semaphore 的典型应用场景,主要集中在需要限制资源访问数量或控制并发访问的场景,例如数据库连接池、文件读写、网络请求等。在这类场景下,Semaphore 能够有效协调多线程对共享资源的访问,从而保障系统的稳定性与整体性能。

1、🌰限流场景

import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;

/**
 * 模拟限流场景
 */
@Slf4j
public class SemaphoreDemo {

    /**
     * 同一时刻最多只允许有两个并发
     */
    private static Semaphore semaphore = new Semaphore(2);
    /**
     * 线程池,模拟并发
     */
    private static Executor executor = Executors.newFixedThreadPool(10);

    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            executor.execute(() -> getInfo());
        }
    }

    public static void getInfo() {
        if (!semaphore.tryAcquire()) {
            log.error("被流控了");
            // TODO: 熔断、限流等
            return;
        }
        try {
            log.info("获取信息成功");
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        } finally {
            semaphore.release();
        }
    }
}

2、🌰输出

14:51:14.490 [pool-1-thread-8] ERROR com.sync.SemaphoreDemo - 被流控了
14:51:14.490 [pool-1-thread-4] ERROR com.sync.SemaphoreDemo - 被流控了
14:51:14.490 [pool-1-thread-1] INFO com.sync.SemaphoreDemo - 获取信息成功
14:51:14.490 [pool-1-thread-10] ERROR com.sync.SemaphoreDemo - 被流控了
14:51:14.490 [pool-1-thread-3] ERROR com.sync.SemaphoreDemo - 被流控了
14:51:14.490 [pool-1-thread-6] ERROR com.sync.SemaphoreDemo - 被流控了
14:51:14.490 [pool-1-thread-9] ERROR com.sync.SemaphoreDemo - 被流控了
14:51:14.490 [pool-1-thread-2] INFO com.sync.SemaphoreDemo - 获取信息成功
14:51:14.490 [pool-1-thread-5] ERROR com.sync.SemaphoreDemo - 被流控了
14:51:14.490 [pool-1-thread-7] ERROR com.sync.SemaphoreDemo - 被流控了

构造函数

Semaphore 可通过构造方法传入布尔参数,可以选择公平锁非公平锁默认采用非公平锁

1、代码

// AQS的实现
private final Sync sync;

// 默认非公平锁
public Semaphore(int permits) {
    sync = new NonfairSync(permits);
}

// 根据布尔值选择使用公平锁还是非公平锁
public Semaphore(int permits, boolean fair) {
    sync = fair ? new FairSync(permits) : new NonfairSync(permits);
}
// 信号量就是AbstractQueuedSynchronizer的state属性值
Sync(int permits) {
    setState(permits);
}

⚠️注意:
✔️信号量赋值到AbstractQueuedSynchronizer的state属性值


四、常用方法

1、acquire

若当前无可用许可证(AbstractQueuedSynchronizer的state可用数量不足),调用该方法的线程会进入阻塞状态,且会持续等待,直至成功获取到所需的许可证(或线程被中断)

    public void acquire() throws InterruptedException {
        sync.acquireSharedInterruptibly(1);
    }

    public final void acquireSharedInterruptibly(int arg)
            throws InterruptedException {
        if (Thread.interrupted())
            throw new InterruptedException();
        //调用 tryAcquireShared(arg) 尝试以共享模式获取锁。
        //AbstractQueuedSynchronizer的state,可用信号量小于0,进入阻塞队列
        if (tryAcquireShared(arg) < 0)
        //调用 doAcquireSharedInterruptibly(arg) 方法,将当前线程加入等待队列并阻塞,直到获取锁或被中断。
            doAcquireSharedInterruptibly(arg);
    }
    // 当前线程加入等待队列并阻塞,直到获取锁或被中断
    private void doAcquireSharedInterruptibly(int arg)
        throws InterruptedException {
        final Node node = addWaiter(Node.SHARED);
        boolean failed = true;
        try {
            for (;;) {
                final Node p = node.predecessor();
               //是否轮到当前节点获取锁
                if (p == head) {
                    int r = tryAcquireShared(arg);
                    if (r >= 0) {
                        setHeadAndPropagate(node, r);
                        p.next = null; // help GC
                        failed = false;
                        return;
                    }
                }
                // shouldParkAfterFailedAcquire 判断是否需要将当前线程挂起
                // parkAndCheckInterrupt注释当前线程,同时返回当前线程中断状态,如果true
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    throw new InterruptedException();
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

1.1、非公平:NonfairSync

        final int nonfairTryAcquireShared(int acquires) {
            for (;;) {
                //获取AbstractQueuedSynchronizer的state,可用信号量
                int available = getState();
                int remaining = available - acquires;
                //可用信号量大于0
                if (remaining < 0 ||
                    compareAndSetState(available, remaining))
                    return remaining;
            }
        }

1.2、公平锁:FairSync

        protected int tryAcquireShared(int acquires) {
            for (;;) {
                // 判断队列中是否有数据,如果有,直接返回-1,小于0,直接同步队列
                if (hasQueuedPredecessors())
                    return -1;
                int available = getState();
                int remaining = available - acquires;
                if (remaining < 0 ||
                    compareAndSetState(available, remaining))
                    return remaining;
            }
        }

⚠️注意:
✔️阻塞状态会进入CLH队列
✔️公平锁与非公平锁的核心区别:
公平锁:若同步队列中已有等待线程,当前线程会直接进入队列排队,严格按照 FIFO 顺序获取锁,保证先来先服务。
非公平锁:当前线程会先直接通过 CAS 操作尝试抢占锁,只有抢占失败时,才会进入同步队列排队。

2、tryAcquire

核心为非阻塞逻辑

  • tryAcquire:若可用许可证数量少于申请数量,线程立即返回 false
  • tryAcquire (long timeout, TimeUnit unit):带超时参数,则会在指定时间内等待许可证,超时仍未获取则返回 false,成功获取则返回 true
    public boolean tryAcquire() {
        // 默认使用了非公平
        return sync.nonfairTryAcquireShared(1) >= 0;
    }
   
    protected int tryAcquireShared(int acquires) {
         return nonfairTryAcquireShared(acquires);
    }

Semaphore.Sync.nonfairTryAcquireShared代码如下

//非公平方法 
final int nonfairTryAcquireShared(int acquires) {
           // 自旋
            for (;;) {
                //获取AbstractQueuedSynchronizer的state,可用信号量
                //信号量小于0,直接返回
                int available = getState();
                int remaining = available - acquires;
                if (remaining < 0 ||
                    compareAndSetState(available, remaining))
                    return remaining;
            }
 }

⚠️注意:
✔️tryAcquire非阻塞逻辑会直接返回,不进入CLH队列
✔️当前线程会先直接通过 CAS 操作尝试抢占锁,抢占失败时,直接返回false

3、release

释放一个许可证,使可用许可证的数量增加,如果一个线程正在尝试获取许可,那么就有一个线程,被选中并获得了刚刚发布的许可证

    public void release() {
        sync.releaseShared(1);
    }

    public final boolean releaseShared(int arg) {
        if (tryReleaseShared(arg)) {
            doReleaseShared();
            return true;
        }
        return false;
    }

    protected final boolean tryReleaseShared(int releases) {
        for (;;) {
            int current = getState();
            //将当前状态值 current 加上要释放的资源数 releases,得到新的状态值 next
            int next = current + releases;
            if (next < current) // overflow
                throw new Error("Maximum permit count exceeded");
            // CAS原子操作,只有当当前状态仍为 current 时,才会将其更新为 next。
            if (compareAndSetState(current, next))
                return true;
        }
    }

    private void doReleaseShared() {
        for (;;) {
            Node h = head;
            // 判断头节点 h 是否不为空且不是尾节点 tail
            if (h != null && h != tail) {
                int ws = h.waitStatus;
                // 如果头节点的等待状态为 Node.SIGNAL
                if (ws == Node.SIGNAL) {
                    //使用 CAS 将其状态从 Node.SIGNAL 设置为 0,如果 CAS 失败,则继续循环重新检查。
                    if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                        continue;
                    //成功后调用 unparkSuccessor(h) 唤醒后继节点。            
                    unparkSuccessor(h);
                }
                //如果头节点的等待状态为 0:
                //使用 CAS 将其状态设置为 Node.PROPAGATE。
                else if (ws == 0 &&
                         !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                    continue;                
            }
            //如果头节点未变,则跳出循环。
            //如果头节点已变,说明有其他线程修改了队列结构,需继续循环处理
            if (h == head)                   
                break;
        }
    }

⚠️注意:
✔️Semaphore 的 release 方法本身不包含显式的队列出队逻辑。
✔️队列节点的出队操作采用延迟执行的策略:
该操作并非在释放许可时触发,而是发生在被唤醒的等待线程成功获取许可之后
线程在 acquire() 方法调用的 doAcquireSharedInterruptibly() 中被阻塞,被唤醒后成功抢占到许可,线程最终通过 setHead() 方法完成节点出队(将当前节点设为队列头,并移除原头节点的引用)。


五、总结

Semaphore(信号量)是高并发场景下十分实用的并发工具类,其核心作用是控制同一时间内访问共享资源的最大线程数。常见应用场景如下:

  1. 接口限流:通过限制并发访问量,对共享资源的请求流量进行控制,保护系统稳定。
  2. 资源池管理:可用于实现数据库连接池、线程池、连接池等有限资源池,统一管理一组稀缺共享资源的分配与释放。