23 - Semaphore 信号量

Semaphore是Java并发工具类,用于控制同时访问特定资源的线程数量。它包括一个计数器和等待队列,通过acquire()获取许可,release()归还许可。Semaphore在初始化时设置许可数量,当许可不足时,线程会被阻塞,释放后唤醒等待线程。常用于数据库连接池等场景,实现流量控制。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

  本篇介绍第三个并发工具类 Semaphore,Semaphore 可以理解为信号量,用于控制资源能够被并发访问的线程数量,以保证多个线程能够合理的使用特定资源。
  

1. Semaphore 模型

  信号量模型还是很简单的,可以简单概括为:一个计数器,一个等待队列,三个方法。在信号量模型里,计数器和等待队列对外是透明的,所以只能通过信号量模型提供的三个方法来访问它们,这三个方法分别是:init()、down() 和 up()。你可以结合下图来形象化地理解。

在这里插入图片描述

这三个方法详细的语义具体如下所示:

  • init():设置计数器的初始值;
  • down():计数器的值减 1;如果此时计数器的值小于 0,则当前线程将被阻塞,否则当前线程可以继续执行;
  • up():计数器的值加 1;如果此时计数器的值小于或者等于 0,则唤醒等待队列中的一个线程,并将其从等待队列中移除。

  这里提到的 init()、down() 和 up() 三个方法都是原子性的,并且这个原子性是由信号量模型的实现方保证的。在 Java SDK 里面,信号量模型是由 java.util.concurrent.Semaphore 实现的,Semaphore 这个类能够保证这三个方法都是原子操作。

  信号量模型里面,down()、up() 这两个操作历史上最早称为 P 操作和 V 操作,所以信号量模型也被称为 PV 原语。另外,还有些人喜欢用 semWait() 和 semSignal() 来称呼它们,虽然叫法不同,但是语义都是相同的。在 Java SDK 并发包里,down() 和 up() 对应的则是 acquire() 和 release()。

  

2. Semaphore 使用

  Semaphore管理着一组许可permit,许可的初始数量通过构造函数设定。

  当线程要访问共享资源时,需要先通过acquire()方法获取许可。获取到之后许可就被当前线程占用了,在归还许可之前其他线程不能获取这个许可。

  调用acquire()方法时,如果没有许可可用了,就将线程阻塞,等待有许可被归还了再执行。

  当执行完业务功能后,需要通过release()方法将许可证归还,以便其他线程能够获得许可证继续执行。

如果初始化了一个许可为1的Semaphore,那么就相当于一个不可重入的互斥锁(Mutex)。

举个例子理解一下:

  我们假设停车场仅有3个停车位,停车位就是有限的共享资源,许可数为3。一开始停车场没有车辆所有车位全部空着,然后先后到来三辆车,停车场车位够,安排进去停车。之后来的车必须在外面候着,直到停车场有空车位。当停车场有车开出去,里面有空位了,则安排一辆车进去(至于是哪辆要看选择的机制是公平还是非公平)。

  从程序角度看,停车场就相当于有限的公共资源,许可数为3,车辆就相当于线程。当来一辆车时,许可数就会减1,当停车场没有车位了(许可数为0),其他来的车辆需要在外面等候着。如果有一辆车开出停车场,许可数+1,然后放进来一辆车。

代码实现如下:

public class TestSemaphore {

    public static void main(String[] args) {
        Parking parking = new Parking(3);
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                parking.park();
            }).start();
        }
    }

    static class Parking {
        Semaphore semaphore;

        public Parking(int count) {
            this.semaphore = new Semaphore(count);
        }

        public void park() {
            try {
                semaphore.acquire();
                long time = (long) (Math.random() * 10);
                System.out.println(Thread.currentThread().getName() 
                + " 进入停车场,停车 + " + time + "秒...");
                Thread.sleep(time);
                System.out.println(Thread.currentThread().getName() 
                + " 开出停车场...");
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                semaphore.release();
            }
        }
    }
}

# 运行结果如下:
Thread-1 进入停车场,停车 + 2...
Thread-2 进入停车场,停车 + 2...
Thread-0 进入停车场,停车 + 9...
Thread-1 开出停车场...
Thread-2 开出停车场...
Thread-3 进入停车场,停车 + 9...
Thread-4 进入停车场,停车 + 1...
Thread-4 开出停车场...
Thread-5 进入停车场,停车 + 3...
Thread-5 开出停车场...
Thread-6 进入停车场,停车 + 0...
Thread-6 开出停车场...
Thread-7 进入停车场,停车 + 9...
Thread-0 开出停车场...
Thread-8 进入停车场,停车 + 3...
Thread-3 开出停车场...
Thread-8 开出停车场...
Thread-9 进入停车场,停车 + 2...
Thread-9 开出停车场...
Thread-7 开出停车场...

  Semaphore 可以用于做流量控制,特别是公共资源有限的应用场景,比如数据库连接。假如有多个线程读取数据后,需要将数据保存在数据库中,而可用的最大数据库连接只有10个,这时候就需要使用 Semaphore 来控制能够并发访问到数据库连接资源的线程个数最多只有10个。在限制资源使用的应用场景下,Semaphore 是特别合适的。

  

3. 源码分析

3.1 类结果

  Semaphore 同样是由 AQS 实现的,用内部类 Sync 来管理锁,Sync 有两个实现,分别为 NonfairSync(非公平锁)和 FairSync(公平锁)。

  这个类结构有没有似曾相识的感觉,重入锁 ReentrantLock 也是同样的类结构,Semaphore 的源码跟 ReentrantLock 有很多相似但又比 ReentrantLock简单。

public class Semaphore implements java.io.Serializable {
    private final Sync sync;
    abstract static class Sync extends AbstractQueuedSynchronizer {}
    static final class NonfairSync extends Sync {}
    static final class FairSync extends Sync {}
}

看下构造方法,设置许可数 permits 其实就是将 AQS.state 设置为 permits:

public Semaphore(int permits) {
    sync = new NonfairSync(permits);
}

NonfairSync(int permits) {
    super(permits);
}

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

  

3.2 acquire

  acquire() 方法就是获取许可,获取到许可就可以继续执行访问共享资源,获取不到就阻塞等待其他线程归还许可。

AQS.state 用来记录可用的许可数量,每获取一个许可 state 减1。

**
 * 获取许可的方法其实就是获取锁的方法
 */
public void acquire() throws InterruptedException {
    sync.acquireSharedInterruptibly(1);
}

public final void acquireSharedInterruptibly(int arg)
        throws InterruptedException {
    // 响应打断
    if (Thread.interrupted())
        throw new InterruptedException();
    
    // 真正获取锁的方法,由Semaphore.NonfairSync实现
    if (tryAcquireShared(arg) < 0)     
    	// 获取锁失败,当前线程阻塞并进入AQS同步队列     
        doAcquireSharedInterruptibly(arg);  
}

/**
 * Semaphore.NonfairSync实现的获取锁的方法
 */
protected int tryAcquireShared(int acquires) {
    return nonfairTryAcquireShared(acquires);
}

/**
 * 每获取一个许可,将state-1,state表示剩余的许可数
 * 如果许可已经用完,返回remaining<0,表示获取不到锁/许可,线程阻塞
 * 如果还有许可,返回remaining>=0,表示获取到锁/许可,线程继续执行
 */
final int nonfairTryAcquireShared(int acquires) {
    for (;;) {
        int available = getState();
        // 每获取一个许可,将state-1,state表示剩余的许可数
        int remaining = available - acquires;
        if (remaining < 0 ||
            compareAndSetState(available, remaining))
            return remaining;
    }
}

  

3.3 release

  release() 方法归还许可,其实就是将 AQS.state 加1。归还成功,唤醒 AQS 队列中等锁的线程,从被阻塞的位置开始执行。

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

/**
 * 释放锁,完全成功,依次唤醒AQS队列中等待共享锁的线程
 */
public final boolean releaseShared(int arg) {
    if (tryReleaseShared(arg)) { // 释放锁,由Semaphore.Sync实现
        doReleaseShared();       // 释放锁成功,唤醒AQS队列中等锁的线程
        return true;
    }
    return false;
}

/**
 * 每归还一个许可将state加1
 */
protected final boolean tryReleaseShared(int releases) {
    for (;;) {
        int current = getState();
        int next = current + releases;// 每归还一个许可将state加1
        if (next < current) // overflow
            throw new Error("Maximum permit count exceeded");
        if (compareAndSetState(current, next))
            return true;
    }
}

  

4. 总结

  信号量 Semaphore 用于控制资源能够被并发访问的线程数量,以保证多个线程能够合理的使用特定资源,比如数据库连接等。

  Semaphore 在构造时设置一个许可数量,这个许可数量用 AQS.state来记录。

  acquire() 方法就是获取许可,只有获取到许可才可以继续执行访问共享资源,获取到许可之后 AQS.state 减1,以记录当前可用的许可数量;如果获取不到许可,线程就阻塞等待其他线程归还许可。

  release() 方法将许可归还,AQS.state 加1;归还之后,唤醒 AQS 队列中阻塞的线程获取许可。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值