信号量Semaphore

4 阅读3分钟

什么是信号量Semaphore?

信号量(Semaphore)JUC中的一个同步工具,用于控制同时访问特定资源的线程数量,通过维护一组许可证(permits) 来实现并发控制。

之前在AQS中了解到有两种模式:独占模式以及共享模式。ReentrantLock就是一个典型的独占模式的实现,而本次的Semaphore就是典型的共享模式的实现。

Semaphore的使用

Semaphore的工作原理

  • 信号量内部维护一个计数器(许可证数量)

  • 线程访问资源前需要获取许可证(acquire()

  • 使用完资源后释放许可证(release()

  • 如果没有可用许可证,线程会被阻塞直到有许可证可用

Semaphore的构造方法

// 创建指定许可证数量的信号量(非公平模式)
Semaphore semaphore = new Semaphore(int permits);

// 创建指定许可证数量的信号量,可指定公平模式
Semaphore semaphore = new Semaphore(int permits, boolean fair);

Semaphore的核心方法

获得许可证

// 阻塞直到获取许可证
void acquire() throws InterruptedException

// 获取指定数量的许可证
void acquire(int permits) throws InterruptedException

// 尝试获取许可证(非阻塞)
boolean tryAcquire()

// 带超时的尝试获取
boolean tryAcquire(long timeout, TimeUnit unit)

// 立即获取,不响应中断
void acquireUninterruptibly()

释放许可证

// 释放一个许可证
void release()

// 释放指定数量的许可证
void release(int permits)

一些查询的方法

// 返回当前可用许可证数
int availablePermits()

// 返回正在等待获取许可证的线程数
int getQueueLength()

// 是否有线程在等待
boolean hasQueuedThreads()

Semaphore的原理

获取许可证acquire

    public void acquire() throws InterruptedException {
        sync.acquireSharedInterruptibly(1);
    }

从调用的函数就可以看出,acquire是一个可以被中断的过程。获得许可证的默认值是1。

    public final void acquireSharedInterruptibly(int arg)
        throws InterruptedException {
        if (Thread.interrupted() ||
            (tryAcquireShared(arg) < 0 &&
             acquire(null, arg, true, true, false, 0L) < 0))
            throw new InterruptedException();
    }

首先会判断中断,其次会调用重写后的tryAcquireShared方法

        protected int tryAcquireShared(int acquires) {
            for (;;) {
                if (hasQueuedPredecessors())
                    return -1;
                int available = getState();
                int remaining = available - acquires;
                if (remaining < 0 ||
                    compareAndSetState(available, remaining))
                    return remaining;
            }
        }

整体是一个自旋循环:

  1. 当同步队列中存在线程时,当前线程不获得锁,因此这是公平锁
  2. 当许可证的余额不足时,直接返回余额(负数)
  3. 当许可证余额充足且获得锁后,返回余额(正数)
  4. 当许可证余额充足但没获取到锁,循环

当这一步获得到锁,则线程已经成功,如果未获取到锁,将继续调用acquire方法。acquire方法详情请见(可重入锁ReentrantLock基础和原理)

ReentrantLock不同的是,这次是共享模式,因此无论是节点在入队之前的尝试获取锁的操作还是被唤醒的节点自旋获取锁的操作都是调用tryAcquireShared

释放许可证release

    public void release() {
        sync.releaseShared(1);
    }

其会调用releaseShared方法

    public final boolean releaseShared(int arg) {
        if (tryReleaseShared(arg)) {
            signalNext(head);
            return true;
        }
        return false;
    }

其中会调用tryReleaseShared释放锁,如果释放成功,将会唤醒同步队列中第一个节点。

        protected final boolean tryReleaseShared(int releases) {
            for (;;) {
                int current = getState();
                int next = current + releases;
                if (next < current) // overflow
                    throw new Error("Maximum permit count exceeded");
                if (compareAndSetState(current, next))
                    return true;
            }
        }

可以看出是自旋cas来释放锁。