java锁-共享模式-信号量Semaphore类介绍解读

Semaphore类结构

public class Semaphore implements java.io.Serializable {
 //定义内部基类静态私有
 private final Sync sync;
 //内部基类 依靠AQS实现
 abstract static class Sync extends AbstractQueuedSynchronizer{} ;
 //非公平锁内部类
 static final class NonfairSync extends Sync {} ;
 //公平锁内部类
 static final class FairSync extends Sync {} ;

//构造函数Semaphore默认是非公平的。
public Semaphore(int permits) {sync = new NonfairSync(permits);}
// 如果需要公平模式 需要显示传入fair=true
public Semaphore(int permits, boolean fair) {
     sync = fair ? new FairSync(permits) : new NonfairSync(permits);
    }

信号量Semaphore类结构和ReentrantLock的类结构大体一致,除了ReentrantLock实现类Lock接口,都是依靠内部类继承AQS抽象类实现同步状态管理的

Semaphore类中内部基类Sync

1.setState(),getPermits()

// Semaphore 的内部同步器(共享模式) 继承AQS 依靠AQS实现同步功能
abstract static class Sync extends AbstractQueuedSynchronizer {
	//根据构造函数传入的参数permits许可值 初始化AQS的state为许可数
    Sync(int ) { setState(permits); }

    // 获取当前剩余许可数
     final int getPermits() { return getState();}

创建 Semaphore 对象时,会调用构造函数并传入许可证数量 permits,state 的初始值就是许可证数量,例如 new Semaphore(5) 的初始 state=5,所以Semaphore 的共享资源多少是我们自型定义的

在AQS的state字段修饰符是volatile的可见性保证:当一个线程修改了volatile变量的值,新值会立即被刷新到主内存中,并且其他线程读取这个变量时可以立即获得最新的值,同时如果state的加减都是CAS 操作,从而确保多线程环境下对 state 的修改是线程安全的

2.nonfairTryAcquireShared(int acquires)方法
非公平模式下共享资源获取的核心实现

    // 非公平模式尝试获取共享许可(供 NonfairSync 使用)
    final int nonfairTryAcquireShared(int acquires) {
        for (;;) {// 自旋(循环直到成功或失败)
            int available = getState(); // 获取当前可用许可证数(AQS的state字段)
            int remaining = available - acquires;// 计算剩余许可证数
            if (remaining < 0 || // 剩余不足,直接返回负数(失败)
            compareAndSetState(available, remaining)) {// CAS尝试更新许可证数
                return remaining; // 返回剩余许可(可能为负)
            }
        }
    }

返回值语义

  • 正数或零:成功获取许可证,返回值表示剩余的许可证数量(可能唤醒后续等待线程)。
  • 负数:获取失败(许可证不足),触发线程进入 AQS 同步队列等待

非公平性以及其余特点
不检查等待队列:新请求的线程直接尝试获取许可证,无需排队,可能“插队”到已阻塞的线程之前
高吞吐量:减少线程切换和排队开销,适用于高并发场景(如短任务快速释放资源)
快速失败机制:如果 remaining < 0(许可证不足),立即返回负数,避免无意义的 CAS 竞争
CAS 原子更新:compareAndSetState(available, remaining) 保证并发环境下许可证数量更新的原子性,若 CAS 失败(其他线程修改了许可证数量),继续循环重试

3.tryReleaseShared(int releases)
释放许可值并进行CAS更新

    // 释放共享许可(公共逻辑)
    protected final boolean tryReleaseShared(int releases) {
        for (;;) {// 自旋循环,确保最终成功
            int current = getState(); // 获取当前许可总数
            int next = current + releases;// 计算释放后的许可总数
            if (next < current) throw new Error("Maximum permit count exceeded");
            if (compareAndSetState(current, next)) {// CAS 更新状态
                return true; // CAS 更新成功
            }
        }
    }

步骤解析
①.自旋循环
使用 for () 无限循环,确保在高并发场景下,即使 CAS 失败也能不断重试,直到成功。
②.获取当前状态
int current = getState() 获取当前信号量的许可总数(即 AQS 的 state 值)。
③.计算新状态
int next = current + releases 计算释放后的许可总数。例如,若当前 current = 2,释放 releases = 3,则 next = 5。
④.溢出检查
if (next < current) 检查是否发生整数溢出(当释放的许可数极大时可能导致 int 越界)。若溢出,抛出 Error 终止程序,避免逻辑错误。
⑤.CAS 更新状态
调用 compareAndSetState(current, next) 原子性地将 state 从 current 更新为 next。若成功,返回 true;若失败(其他线程已修改状态),继续循环重试。(始终返回 true(除非溢出抛出异常),因为 Semaphore 的释放操作理论上不会失败(与 acquire 不同,释放总能完成))

4.reducePermits(int reductions)
动态减少信号量的总许可数量

        final void reducePermits(int reductions) {
            for (;;) {// 自旋循环 确保CAS操作成功
                int current = getState();// 获取当前可用许可证数(AQS的state字段)
                int next = current - reductions;// 计算减少后的剩余许可证数
                if (next > current) // 处理溢出(如reductions 为负数)
                    throw new Error("Permit count underflow");
                if (compareAndSetState(current, next))// CAS 原子更新状态
                    return;//退出循环
            }
        }

1.自旋 CAS 操作:通过循环和 CAS 原子更新状态,确保线程安全。
2.校验合法性:若 reductions 为负或导致 next > current(如溢出),抛出异常。
3.更新许可值:将总许可数从 current 减少为 next = current - reductions

好处:
1.动态调整: 允许运行时减少许可总量,无需重新创建 Semaphore 对象
2.不影响已获取许可的线程:已调用 acquire() 的线程可继续持有许可,但后续线程可获取的许可减少
3.允许减少后的许可为负:若减少后的许可为负,后续 acquire() 会阻塞,直到许可被释放且足够
4.线程安全:依赖 AQS 的 CAS 操作,确保并发调用的安全性。

注意事项
1.参数校验:reduction 必须为非负数,否则抛出 IllegalArgumentException。
2.潜在死锁:若过度减少许可导致可用许可长期为负,且无线程释放,可能引发线程饥饿。

5.drainPermits()
用于立即获取并返回当前所有可用的许可(Permits),将剩余的许可数量清零

    // 清空所有许可(返回实际清空的数量)
    final int drainPermits() {
        for (;;) {// 自旋循环 确保在并发竞争下最终成功。
            int current = getState();// 获取当前可用许可证数(AQS的state字段)
            if (current == 0 || compareAndSetState(current, 0)) {
            //将状态从当前值 current 原子性地更新为 0,若成功则返回原有许可数量。
                return current;
            }
        }
    }
}

这个方法将当前剩余的许可值拿过来占着,比如剩余3个,同时将许可值更新为0,这样其余的线程都没办法使用了,而这3个我拿过来啥也不干就占着,需要恢复调用 release()方法
特性
非阻塞: 立即返回当前剩余许可,无需等待。
原子性: 通过 CAS 保证线程安全。
忽略公平性: 不检查等待队列中的线程,直接抢占所有许可(无论信号量是公平模式还是非公平模

使用场景:当需要暂停某个资源的使用,直到某个条件满足时,可以使用这个方法
资源重置:需要立即回收所有可用资源(例如重置连接池)。
统计剩余资源:快速获取当前可用许可数量(但会直接消耗这些许可)。
强制资源释放:在需要确保后续操作从零开始时使用(如测试环境)。

注意事项
与公平性的冲突:公平模式,drainPermits() 仍会直接抢占所有许可,可能导致等待队列中的线程饥饿
许可恢复:调用 drainPermits() 后,如果需要恢复许可,需显式调用 release()

// 1. 清空许可
int drained = semaphore.drainPermits();
// 2. 业务逻辑处理(如暂停资源访问)
// 3. 显式恢复许可
semaphore.release(drained); // 返还所有借用的许可

返回值语义:返回的是被清空的许可数量(非负值),而非剩余的许可数
不触发唤醒:若其他线程因许可不足正在等待(如 acquire()),调用 drainPermits() 不会唤醒它们。

reducePermits与 drainPermits 的区别
drainPermits():清空所有可用许可(立即返回当前可用值)。
reducePermits():减少总量,可能使获取许可tryAcquireShared返回的可用许可值为负。

非公平锁内部类NonfairSync

    static final class NonfairSync extends Sync {
        private static final long serialVersionUID = -2694183684443567898L;
		// 构造函数 传参值 许可值
        NonfairSync(int permits) {
            super(permits);
        }

		// 非公平锁的尝试获取锁逻辑是Sync.nonfairTryAcquireShared方法
        protected int tryAcquireShared(int acquires) {
            return nonfairTryAcquireShared(acquires);
        }
    }

nonfairTryAcquireShared 这个方法在上面Sync内部类中介绍了,这里不复述了,关键点就是非公平体现:不检查等待队列:新请求的线程直接尝试获取许可证,无需排队

公平锁内部类FairSync

    static final class FairSync extends Sync {
        private static final long serialVersionUID = 2014338818796000944L;
		// 构造函数 传参值 许可值
        FairSync(int permits) {
            super(permits);
        }
		// 公平模式获取许可:重写AQS里面的钩子方法tryAcquireShared
        protected int tryAcquireShared(int acquires) {
            for (;;) {//自旋检查 直到有返回值 负值代表失败 正值代表成功
                if (hasQueuedPredecessors())/// 关键:检查是否有等待线程
                    return -1;// 返回-1 代表当前线程需排队
                int available = getState();// 获取当前可用许可证数(AQS的state字段)
                int remaining = available - acquires;// 计算剩余许可证数
                if (remaining < 0 || // 剩余不足,直接返回负数(失败)
                    compareAndSetState(available, remaining))// CAS尝试更新许可证数
                    return remaining;// 返回剩余许可(可能为负)

            }
        }
    }

比起非公平锁 多了核心步骤就是获取许可值资源之前,检查前面是否有等待线程,如果有则直接返回-1 代表尝试获取许可失败,开始入队等待,如果前面没有等待线程,那就和非公平锁逻辑一样开始计算剩余许可数够不够用 以及cas操作更新等

Semaphore类其余方法体

1. acquire() 方法族

// 获取 1 个许可(阻塞,可中断)
public void acquire() throws InterruptedException {
    sync.acquireSharedInterruptibly(1); // 调用 AQS 方法
}

// 获取 N 个许可(阻塞,可中断)
public void acquire(int permits) throws InterruptedException {
    if (permits < 0) throw new IllegalArgumentException();
    sync.acquireSharedInterruptibly(permits);
}

底层调用链:
acquireSharedInterruptibly() → tryAcquireShared()(由 FairSync 或 NonfairSync 实现) → 若失败则线程进入 AQS 等待队列

2. tryAcquire() 方法族

// 非阻塞尝试获取 1 个许可
public boolean tryAcquire() {
    return sync.nonfairTryAcquireShared(1) >= 0; // 直接尝试,无需排队
}

// 超时尝试获取许可
public boolean tryAcquire(long timeout, TimeUnit unit)
    throws InterruptedException {
    return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
}

非公平特性:tryAcquire() 直接调用 nonfairTryAcquireShared(),允许插队。
公平特性:最终调用FairSync.tryAcquireShared,先检查等待队列是否存在

3. release() 方法族

// 释放 1 个许可
public void release() {
    sync.releaseShared(1); // 调用 AQS 方法
}

// 释放 N 个许可
public void release(int permits) {
    if (permits < 0) throw new IllegalArgumentException();
    sync.releaseShared(permits);
}

底层逻辑:
releaseShared() → tryReleaseShared()(CAS 增加 state) → 唤醒等待队列中的线程。

4. availablePermits()

// 返回当前可用许可数
public int availablePermits() {
    return sync.getPermits();//getState
}

semaphore 共享模式的工作流程

简单代码示例

   //默认一个许可值为3的信号量
   Semaphore semaphore = new Semaphore(3);
    @Test
    public void test() throws InterruptedException {
        //继承Runnable
        Runnable r = new Runnable() {
            public void run() {
                try {
                    System.out.println(Thread.currentThread().getName() + " 尝试获取锁开始工作...");
                    semaphore.acquire();
                    System.out.println(Thread.currentThread().getName() + " 成功获取到锁并剩余许可数:" + semaphore.availablePermits());
                    //模拟工作耗时2秒
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }finally {
                    // 3. 释放许可
                    semaphore.release();
                    System.out.println(Thread.currentThread().getName() + " 释放许可值,当前许可数:" + semaphore.availablePermits());
                }
            }
        };

        // 创建5个线程模拟并发请求
        List<Thread> threads = new ArrayList<>();
        for (int i = 1; i <= 5; i++) {
            Thread thread = new Thread(r,("Thread-" + i));
            thread.start();
            threads.add(thread);
        }
		// join()的调用者线程主线程 会等待目标子线程执行完毕,防止主线程执行完毕过快,子线程没执			  行完就结束了
        for (Thread t : threads) {
            t.join();
        }
    }

控制台打印

Thread-1 尝试获取锁开始工作...
Thread-3 尝试获取锁开始工作...
Thread-2 尝试获取锁开始工作...
Thread-2 成功获取到锁并剩余许可数:0
Thread-1 成功获取到锁并剩余许可数:1
Thread-3 成功获取到锁并剩余许可数:1
Thread-5 尝试获取锁开始工作...
Thread-4 尝试获取锁开始工作...
Thread-1 释放许可值,当前许可数:3
Thread-5 成功获取到锁并剩余许可数:2
Thread-4 成功获取到锁并剩余许可数:1
Thread-3 释放许可值,当前许可数:3
Thread-2 释放许可值,当前许可数:3
Thread-4 释放许可值,当前许可数:2
Thread-5 释放许可值,当前许可数:3

acquire() 执行流程
最先开始工作的线程通过
semaphore.acquire() → acquireSharedInterruptibly(1) → 该方法通过(tryAcquireShared(arg) < 0)判断

通过tryAcquireShared(arg) < 0 判断
tryAcquireShared()(由 FairSync 或 NonfairSync 实现) 返回剩余可用许可值(可为负)
为ture 说明当前没有许可值 进入AQS同步队列等待doAcquireSharedInterruptibly
为false 说明当前线程已经拿到许可值执行任务了(CAS操作变更许可值了) 不需要入队了

入队方法doAcquireSharedInterruptibly,先入同步队列,然后自旋判断 是否可以拿到许可值恢复线程执行并更新成头节点 或者 挂起

release()执行流程
拿到许可值的线程最终执行完finally 释放锁
semaphore.release()→ releaseShared() → 该方法通过 tryReleaseShared(arg)判断

tryReleaseShared()(CAS 增加 state)
返回ture 代表释放增加了当前可用许可值
没有false的返回,如果释放过程出错 抛出错误Error

然后执行doReleaseShared()方: 唤醒等待队列中的线程
该方法通过自旋方式 从头节点唤醒标识 开始唤醒下一个节点,unparkSuccessor(head.next);从而恢复上面那些没拿到许可值进入同步队列被挂起的线程,从而继续执行 这样就闭环喽

闭环的关键点
doAcquireSharedInterruptibly方法里的parkAndCheckInterrupt() 里挂起当前线程
doReleaseShared 方法里unparkSuccessor(h) 唤醒后续节点的线程解挂

Semaphore类使用注意事项
示例:控制最多 10 个线程并发访问资源

Semaphore semaphore = new Semaphore(10, true); // 公平模式
void accessResource() throws InterruptedException {
    semaphore.acquire();
    try {
        // 访问共享资源
    } finally {
        semaphore.release();
    }
}
  • 避免死锁:确保 release() 在 finally 块中调用。
  • 许可数量管理:不要过度释放(release() 次数超过 acquire())
  • 性能权衡:非公平模式适用于高吞吐场景,公平模式适用于资源竞争激烈且需公平性的场景
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值