【033】手写 AQS 版 Semaphore:门禁 “多人通行” 权限怎么实现?

请添加图片描述
📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌

📙 作者: 编程技术圈(哇哥面试陪跑)
👉 欢迎关注、分享、评论
✔️ 持续分享更多干货内容
🌐🌏🌎➕tcmeta, 欢迎沟通交流

📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌

零、引入

“并发访问又超了!” 王二盯着服务器监控直跺脚,他写的接口允许 10 个线程同时访问,但高峰期直接冲进来 20 个,数据库连接被占满,系统响应超时。

领导把需求甩给他:“用 Semaphore 控制并发数,最多 10 个线程同时进,再自己手写一个,搞懂底层!”

哇哥端着保温杯凑过来:“Semaphore 就是‘门禁通行权限’,比如公司门禁一次允许 10 个人进,用完权限就排队,用完再还回来。它也是基于 AQS 实现的,只不过是‘共享锁’—— 今天咱们先用 JDK 的 Semaphore 救急,再手写一个,让你彻底吃透 AQS 的共享锁逻辑!”

点赞 + 关注,跟着哇哥和王二,从使用到手写,精通 Semaphore,并发控制再也不慌!
在这里插入图片描述

一、先救急:JDK 的 Semaphore 怎么用?(5 分钟上手)

Semaphore 的核心是 “共享许可”,就像公司门禁有 10 个通行权限,员工刷一次卡拿一个权限,用完还回去 —— 对应代码里的acquire()拿许可,release()还许可。

👉 实战:用 Semaphore 控制接口并发数


在这里插入图片描述

运行结果:并发数被精准控制在 10

✨ 核心亮点

  • 20 个线程抢 10 个许可,超过的自动排队,不会出现连接池被占满的情况;
  • acquire()可中断、可超时(acquire(1, TimeUnit.SECONDS)),比 Lock 更灵活;
  • 支持公平锁(new Semaphore(10, true)),按排队顺序拿许可,避免线程饥饿。

二、Semaphore 的底层:AQS 的 “共享锁” 逻辑

请添加图片描述

👉 核心源码

王二好奇 Semaphore 为啥能支持 “多人同时拿许可”,哇哥直接扒开源码:“Semaphore 是 AQS 的共享锁实现,和 ReentrantLock 的独占锁不同,它的tryAcquireShared方法返回剩余许可数,支持多个线程同时获锁。”
在这里插入图片描述
在这里插入图片描述

‼️ 用门禁场景讲透共享锁 vs 独占锁

在这里插入图片描述

📌 Semaphore 的 AQS 核心逻辑

在这里插入图片描述
Semaphore 的 AQS 子类重写了tryAcquireShared和tryReleaseShared,核心是 “许可数的增减”:

  • tryAcquireShared:尝试拿许可,返回剩余许可数(>=0 代表成功,<0 代表失败);
  • tryReleaseShared:尝试还许可,返回是否释放成功,成功后唤醒排队线程。

简化版源码逻辑(Semaphore 的 Sync 类)

// Semaphore的AQS子类(共享锁逻辑)
private static class Sync extends AbstractQueuedSynchronizer {
    Sync(int permits) {
        setState(permits); // state存许可总数
    }

    // 拿许可:共享锁核心方法
    @Override
    protected int tryAcquireShared(int acquires) {
        for (;;) { // 死循环防虚假唤醒
            int available = getState();
            int remaining = available - acquires;
            // 剩余许可不足,返回负数(失败)
            if (remaining < 0 ||
                // CAS修改许可数
                compareAndSetState(available, remaining)) {
                return remaining;
            }
        }
    }

    // 还许可:共享锁核心方法
    @Override
    protected boolean tryReleaseShared(int releases) {
        for (;;) {
            int available = getState();
            int next = available + releases;
            // 许可数不能超过总数(防止重复release)
            if (next < available) {
                throw new Error("许可数溢出");
            }
            // CAS修改许可数
            if (compareAndSetState(available, next)) {
                return true; // 释放成功,唤醒排队线程
            }
        }
    }
}

“看到没?共享锁的核心是tryAcquireShared返回剩余许可数,” 哇哥强调,“AQS 会根据返回值判断是否需要排队 —— 如果剩余许可>=0,当前线程获锁成功;如果 < 0,就进 CLH 队列阻塞。”

在这里插入图片描述

三、手写 AQS 版 Semaphore:自己造 “门禁通行权限”

在这里插入图片描述
光看源码不够,哇哥带着王二手写一个基于 AQS 的 Semaphore,核心就是重写共享锁的两个方法,剩下的排队、唤醒全靠 AQS。

package cn.tcmeta.aqs;


import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.AbstractQueuedSynchronizer;

/**
 * @author: laoren
 * @description: 手写 Semaphore 完整代码
 * @version: 1.0.0
 */
public class MySemaphore {
    // 核心:AQS共享锁子类
    private final Sync sync;

    // 构造方法:指定许可数
    public MySemaphore(int permits) {
        if (permits <= 0) {
            throw new IllegalArgumentException("许可数必须大于0");
        }
        this.sync = new Sync(permits);
    }

    // AQS共享锁逻辑
    private static class Sync extends AbstractQueuedSynchronizer {
        // 初始化许可数(state=permits)
        Sync(int permits) {
            setState(permits);
        }

        // 1. 拿许可(共享锁核心)
        @Override
        protected int tryAcquireShared(int acquires) {
            for (; ; ) {
                int available = getState();
                int remaining = available - acquires;
                // 剩余许可不足,返回负数(进队列排队)
                if (remaining < 0 ||
                        // CAS修改许可数(线程安全)
                        compareAndSetState(available, remaining)) {
                    return remaining;
                }
            }
        }

        // 2. 还许可(共享锁核心)
        @Override
        protected boolean tryReleaseShared(int releases) {
            for (; ; ) {
                int available = getState();
                int nextPermits = available + releases;
                // 防止重复释放导致许可数溢出
                if (nextPermits < available) {
                    throw new IllegalMonitorStateException("重复释放许可");
                }
                // CAS修改许可数
                if (compareAndSetState(available, nextPermits)) {
                    return true; // 释放成功,AQS会唤醒排队线程
                }
            }
        }

        // 辅助方法:获取当前可用许可数
        int getAvailablePermits() {
            return getState();
        }
    }

    // 拿1个许可(阻塞)
    public void acquire() throws InterruptedException {
        sync.acquireSharedInterruptibly(1);
    }

    // 拿1个许可(非阻塞,立即返回)
    public boolean tryAcquire() {
        return sync.tryAcquireShared(1) >= 0;
    }

    // 还1个许可
    public void release() {
        sync.releaseShared(1);
    }

    // 获取可用许可数
    public int availablePermits() {
        return sync.getAvailablePermits();
    }

    // 测试:用手写Semaphore控制并发
    static void main() {
        // 10个许可(最多10个线程同时访问)
        MySemaphore semaphore = new MySemaphore(10);
        ExecutorService pool = Executors.newFixedThreadPool(20);

        for (int i = 1; i <= 20; i++) {
            int userId = i;
            pool.submit(() -> {
                try {
                    semaphore.acquire();
                    System.out.println(Thread.currentThread().getName() + ":用户" + userId + "访问成功,可用许可=" + semaphore.availablePermits());
                    TimeUnit.MILLISECONDS.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    semaphore.release();
                    System.out.println(Thread.currentThread().getName() + ":用户" + userId + "访问完成,可用许可=" + semaphore.availablePermits());
                }
            });
        }

        pool.shutdown();
    }
}

在这里插入图片描述

👉 核心亮点(哇哥划重点)

  1. 我们只重写了 AQS 的tryAcquireShared和tryReleaseShared,就实现了共享锁的核心逻辑;
  2. AQS利用LockSupport方法会自动处理 “抢锁 - 入队 - 阻塞”,不用我们写一行 LockSupport 代码;
  3. 支持中断:acquire方法可响应线程中断,比 JDK 早期的并发工具更灵活。
    在这里插入图片描述

四、AQS 的终极价值:一套骨架,多种锁实现

在这里插入图片描述

王二写完手写 Semaphore,终于明白 AQS 的强大之处 —— 一套骨架能支撑不同类型的锁

  • 独占锁(ReentrantLock):重写tryAcquire/tryRelease,state=0/1;
  • 共享锁(Semaphore):重写tryAcquireShared/tryReleaseShared,state = 剩余许可数;
  • 倒计时锁(CountDownLatch):重写tryAcquireShared,state = 倒计时次数,减到 0 才允许获锁。

‼️ AQS 的核心设计思想:模板方法模式

AQS 用 “模板方法模式” 把锁的逻辑分成两部分:

  • 模板方法:AQS 自带的acquire/release等方法,定义锁的整体流程;
  • 钩子方法:留给子类重写的tryAcquire/tryAcquireShared等方法,定义具体的抢锁 / 放锁逻辑。

“这就像盖房子,AQS 帮你把钢筋骨架搭好,” 哇哥比喻,“你只需要根据需求砌砖墙(独占锁)或装玻璃(共享锁),不用从零开始盖。

五、AQS 全系列核心回顾

➡️ 小总结

  1. 顶层骨架(AQS):模板方法模式,提供排队、阻塞、唤醒的通用逻辑;
  2. 底层实现(LockSupport):AQS 的 “手脚”,实现线程的精准阻塞和唤醒;
  3. 上层应用(ReentrantLock/Semaphore):基于 AQS 的具体工具,重写钩子方法实现特定功能。

📢 哇哥的终极彩蛋

“面试时被问 AQS,别光说定义,” 哇哥传授面试技巧,“先讲‘总控台 + 对讲机’的比喻,再画 CLH 队列的结构,最后说‘我手写过基于 AQS 的 Semaphore’—— 拿出代码示例,面试官绝对对你刮目相看。记住:并发编程的核心是‘懂底层,会实战’,而不是背 API。”

✔️ 最后说句实在的

AQS 是 Java 并发的 “灵魂”,搞懂它,你就掌握了所有并发工具的底层逻辑。今天的手写 Semaphore,虽然是简化版,但核心逻辑和 JDK 源码一致 —— 能把这个写出来,你就已经超过了 80% 只会调 API 的程序员。

如果这篇帮你彻底吃透了 AQS,点赞 + 分享给你那还在死记硬背并发工具用法的同事!

请添加图片描述
在这里插入图片描述
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值