📖 Java 并发编程专栏持续更新中

从 JMM → volatile → CAS → synchronized → AQS → ReentrantLock → Semaphore,逐步深入 Java 并发底层原理。

更多原创技术文章可在公众号历史消息中查看,欢迎关注交流。🚀

文章目录

一、什么是 Semaphore

二、应用场景

三、Semaphore核心API

四、Semaphore内部原理

五、获取许可证过程

六、释放许可证过程

七、公平与非公平模式

八、与ReentrantLock区别

九、总结

一、什么是 Semaphore

Semaphore(信号量)是 JDK 提供的一个并发工具类,用来控制同时访问某个资源的线程数量

位于:java.util.concurrent.Semaphore

例如:Semaphore semaphore = new Semaphore(3);

表示:

当前资源有3个许可证(Permit)

线程1 获取许可证
线程2 获取许可证
线程3 获取许可证

此时许可证耗尽

线程4必须等待

当某个线程释放许可证:

semaphore.release();

线程4才能继续执行

二、应用场景

场景1:数据库连接池

假设连接池:最大连接数 = 30

那么:

Semaphore semaphore =
        new Semaphore(30);

获取连接:semaphore.acquire();

归还连接:semaphore.release();

保证:最多30个线程同时访问数据库

场景2、限流(限制并发量)

限制接口并发数:最多100个请求同时处理

Semaphore semaphore =
        new Semaphore(100);

场景3、爬虫并发控制

Semaphore semaphore =
        new Semaphore(10);

同时最多10个线程抓取网页

避免:把目标网站打挂

 简单示例:允许3个线程同时执行

public class SemaphoreDemo {

    private static final Semaphore semaphore =
            new Semaphore(3);

    public static void main(String[] args) {

        for(int i=1;i<=10;i++){

            int num=i;

            new Thread(() -> {

                try {

                    semaphore.acquire();

                    System.out.println(
                            num + " 获取许可证");

                    Thread.sleep(3000);

                } catch (Exception e) {
                    e.printStackTrace();
                } finally {

                    semaphore.release();

                    System.out.println(
                            num + " 释放许可证");
                }

            }).start();
        }
    }
}

输出类似:

1 获取许可证
2 获取许可证
3 获取许可证

(其余线程等待)

1 释放许可证

4 获取许可证

说明:

最多只有3个线程同时执行

三、Semaphore核心API

在说核心API前先说一下下面频繁出现的permit是什么

permit是什么

通过前面学习 ReentrantLock 我们知道,AQS 内部维护了一个 state 变量

在 ReentrantLock 中:

state = 0    表示未加锁
state = 1    表示已加锁
state > 1    表示锁重入次数

那么 Semaphore 中经常提到的 permit(许可证) 又是什么呢

查看 Semaphore 源码:

Sync(int permits) {
    setState(permits);
}

继续查看 setState() 方法,会发现它来自 AQS:

protected final void setState(int newState) {
    state = newState;
}

因此可以发现,Semaphore 并没有单独维护一个 permit 变量

当我们创建:

Semaphore semaphore = new Semaphore(3);

时,实际上执行的是:

state = 3;

这里的 3 就表示当前可用的许可证数量

在 Semaphore 中:

state = 剩余可用许可证数量(Permit Count)

例如:

state = 3    剩余3个许可证
state = 2    剩余2个许可证
state = 1    剩余1个许可证
state = 0    没有可用许可证

每当线程执行:

semaphore.acquire();

本质上就是尝试将:state--

而执行:

semaphore.release();

本质上就是:state++

因此可以理解为:

Permit(许可证)
        ↓
由AQS中的state维护

ReentrantLock:
state = 锁状态/重入次数

Semaphore:
state = 剩余许可证数量

acquire()

获取许可证

semaphore.acquire();

流程:

有许可证
    ↓
permit--
    ↓
继续执行

没有许可证
    ↓
进入AQS队列
    ↓
阻塞等待

release()

释放许可证

semaphore.release();

流程:

permit++
    ↓
唤醒等待线程

tryAcquire()

尝试获取许可证

if(semaphore.tryAcquire()){
    // 获取成功
}

不会阻塞

成功 → true

失败 → false

acquire(int n)

一次获取多个许可证

semaphore.acquire(3);

表示:

必须一次拿到3个Permit

否则等待

availablePermits()

查看剩余许可证

int count = semaphore.availablePermits();

四、Semaphore内部原理

实际上和 ReentrantLock 非常像:

Semaphore = AQS + state

AQS中的state

在 ReentrantLock 中:

state = 持有锁次数

例如:

state = 0 未加锁
state = 1 已加锁
state = 2 重入两次

Semaphore 中:

state = 剩余许可证数量

例如:
new Semaphore(3);

表示初始化state = 3

为什么使用共享模式

 AQS有两种模式:

独占模式(Exclusive)


共享模式(Shared)

ReentrantLock:

独占模式

因为:同一时刻只能一个线程持有锁

Semaphore:

共享模式

因为:同一时刻允许多个线程成功获取资源

例如:

new Semaphore(5);

可以同时:

线程A成功

线程B成功

线程C成功

线程D成功

线程E成功

所以必须使用:AQS Shared Mode

五、获取许可证过程

假设:state = 3

线程A执行:acquire()

AQS内部:

具体源码见:Semaphore.java文件

链路:

acquire()
            ↓
acquireSharedInterruptibly(1)
            ↓
Sync.tryAcquireShared()
            ↓
NonfairSync 或 FairSync


final int nonfairTryAcquireShared(int acquires) {

    for (;;) {

        int available = getState();

        int remaining = available - acquires;

        if (remaining < 0 ||
            compareAndSetState(available, remaining))
            return remaining;
    }
}

结果:

state: 3 → 2

成功获得许可证

线程B:state: 2 → 1

线程C:state: 1 → 0

线程D:state = 0

再执行:acquire()

发现:remaining < 0 获取失败

进入:AQS同步队列

然后:LockSupport.park(); 挂起

六、释放许可证过程

线程A执行:

release();

内部:

protected final boolean tryReleaseShared(int releases) {

    for (;;) {

        int current = getState();

        int next = current + releases;

        if (compareAndSetState(current, next))
            return true;
    }
}

例如:state : 0 → 1

然后:doReleaseShared();   唤醒等待线程

七、公平与非公平模式

非公平(默认)

new Semaphore(5);

等价于:

new Semaphore(5,false);

谁抢到谁先执行

吞吐量最高

公平模式

new Semaphore(5,true);

严格FIFO

队列:

A
B
C
D

释放许可证后,必须A先获取,不能插队

八、与ReentrantLock区别

对比项 ReentrantLock Semaphore
本质 信号量
AQS模式 独占 共享
同时允许线程数 1 N
state含义 重入次数 剩余许可证
应用场景 临界区保护 流量控制

九、总结

Semaphore 本身代码并不复杂,核心思想只有一句话:

把AQS中的state当作许可证数量使用

获取许可证:

CAS(state--)


成功 -> 执行

失败 -> 进入AQS队列等待

释放许可证:

CAS(state++)


成功 -> 唤醒等待线程

因此从源码角度看:

ReentrantLock:
state = 锁重入次数

Semaphore:
state = 剩余许可证数量

更多推荐