Java使用信号量(Semaphore)实现阻塞容器

73 阅读3分钟

1.什么是信号量

学计算机的小伙伴应该都记得以前在操作系统课程中被信号量的P,V操作折磨的日子,当时只是抽象的说这里需要P,那里需要V的,很难结合实际的应用去使用信号量。而在Java中就实现了一种特别好用的信号量Semaphore。Java中的Semaphore大多数情况下可以作为为计数信号量,用来控制同时访问某个特定资源的操作数量,或者是执行某个特定操作的数量。计数信号量还可以用来实现某种资源池,或者是对容器添加边界。

Semaphore中管理着一组虚拟的许可,许可的初始数量可以通过构造函数来指定。在执行操作时可以首先获得许可,在使用过后释放许可,如果没有许可,那么aquire将阻塞直到有许可(或者直到被中断或者操作超时)。release方法将返回一个许可给信号量。

Semaphore也可以作为计算信号量,计算信号量的一种简化形式就是二值信号量,即初始值为1的Semaphore,二值信号量可以用作互斥体(mutex)并具备不可重入的加锁语义,谁拥有这个唯一许可,就拥有了互斥锁

2.信号量的应用场景

Semaphore可以用于实现资源池,例如数据库连接池,我们可以构造一个固定长度的资源池,当池为空时,请求资源将会失败,但是我们真正希望看到的行为时阻塞而不是失败,并且当池非空的时候解除阻塞,如果将Semaphore的计数值初始化为池的大小,并在池中获取一个资源之前首先调用acquire方法获取一个许可,将资源返回给资源池之后调用release释放许可。

3.示例解析

我们可以使用Semaphore将任何一种容器变成有界的阻塞容器。例如,我们可以利用Semaphore将一个Set容器改造为一个阻塞容器,代码如下:

public class BoundedHashSet<T> {
    private final Set<T> set;
    private final Semaphore sem;

    public BoundedHashSet(int bounded) {
        this.set = Collections.synchronizedSet(new HashSet<T>());
        sem = new Semaphore(bounded);
    }

    public boolean add(T e) throws InterruptedException {
        sem.acquire();
        boolean wasAdded = false;
        try {
            wasAdded = set.add(e);
            return wasAdded;
        } finally {
            if (!wasAdded) {
                sem.release();
            }
        }
    }

    public boolean remove(T e) {
        boolean wasRemoved = set.remove(e);
        if (wasRemoved) {
            sem.release();
        }
        return wasRemoved;
    }

    public static void main(String[] args) throws InterruptedException {
        BoundedHashSet<Integer> boundedHashSet = new 
        BoundedHashSet<>(3);
        
        boundedHashSet.add(1);
        boundedHashSet.add(2);
        boundedHashSet.add(3);
        boundedHashSet.add(4);
        boundedHashSet.remove(3);
    }
}

如上面代码所示,信号量的计数值会初始化为容器的的最大值,add操作在向底层容器中添加一个元素之前,首先要获取一个许可。如果add操作没有添加任何元素没那么会释放许可。同样,remove操作会释放一个许可,使更多的元素能够添加到容器中,底层的Set实现并不知道关于边界的任何信息,这些都是由BoundedHashSet来处理。