24.Semaphore的作用和原理

1.Semaphore的功能和作用

Semaphore就是信号灯的意思,主要功能是用来限制对某个资源同时访问的线性数量,它有两个核心方法:

  • acquire()方法,获取一个令牌。

  • release()方法,释放一个令牌。

如下图所示,当多个线程访问某个限制访问流量的资源时,需要先调用acquire()方法获得一个访问令牌,如果能正常获得,则表示允许访问,如果令牌不够,则会阻塞当前线程。当某个获得令牌的线程通过release()方法释放一个令牌后(令牌数量是固定的),被阻塞在acquire()方法的线程就有机会获得这个释放的令牌,从而获得访问权限。

我们看一个使用Semaphore的例子

public class SemaphoreExample {

    public static void main(String[] args) {
        Semaphore semaphore=new Semaphore(2);
        ExecutorService service= Executors.newCachedThreadPool();
        for (int i = 0; i < 1000; i++) {
            service.execute(new SomeTask(semaphore));
        }
        service.shutdown();
    }
    static class SomeTask implements Runnable{
        private Semaphore semaphore;
        public SomeTask(Semaphore semaphore){
            this.semaphore=semaphore;
        }
        @Override
        public void run(){
            try {
                semaphore.acquire();
                System.out.println(Thread.currentThread().getName()+" 获得一个令牌");
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }finally {
                System.out.println(Thread.currentThread().getName()+" 释放一个令牌");
                semaphore.release(1000);
            }
        }
    }
}

这里定义了数量为2的令牌实例,然后定义了一个线程池来执行SomeTask任务。在SomeTask中,使用了semaphore.acquire()方法来限制最大访问线程数量,用来模拟远远超过令牌数的线程来访问SomeTask的场景。

Semaphore方法的核心就是一个许可证管理,通过acquire()方法获得一个许可证,通过release()方法释放一个许可证,实际上并没有真实的令牌发给线程,只是维护了一个可分配数量进行计数维护。

在Semaphore中有两个接口,一起看一下:

  • Semaphore(int permits, boolean fair),permits就是令牌数,fair表示公平性,也就是在令牌被释放的临界点是否允许提前抢占到令牌。

  • acquire(int permits) :获取指定数量的令牌,如果数量不足,则会阻塞当前线程。

  • tryAcquire(int permits) :尝试获取指定数量的令牌,此过程是非阻塞的,如果令牌数不够就返回false。

  • release(int permits):释放指定permits数量的令牌。

  • drainPermits():当前线程获得剩下的所有可用令牌。

  • hasQueuedThread():判断当前Semaphore实例上是否存在正在等待令牌的线程。

Semaphore常见的应用场景就是实现线程之间的限流,或者限制某些共享资源的访问数量。

2 Semaphore原理分析

Semaphore实际上也是基于AQS的共享锁来实现的,因为在Semaphore中允许多个线程获得令牌被唤醒。所以在基于AQS的实现上我们可以推测出,在构建Semaphore实例时传递的参数是permits,其实还是AQS中state属性,假设初始化是permits=5,那么每次调用release()方法,都是针对state进行递减。因此当state=5时,意味着所有的令牌都已经用完,后续的线程都会以共享锁类型添加到CLH队列中,而当state<5时,说明已经有其他线程获得令牌了,可以从CLH队列中唤醒头部的线程。

从根本上说,Semaphore就是通过重写AQS中的下面两个方法来实现不同的业务场景的。

  • tryAcquireShared()方法:抢占共享锁。

  • tryReleaseShared()方法:释放共享锁。

2.1 令牌获取过程

由于共享锁的整体源码已经分析过了,这里只列出Semaphore中不一样的内容。

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

可以看到默认Semaphore是非公平策略,我们继续看NonfairSync类。

static final class NonfairSync extends Sync {
    private static final long serialVersionUID = -2694183684443567898L;

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

    protected int tryAcquireShared(int acquires) {
        return nonfairTryAcquireShared(acquires);
    }
}

在非公平同步策略中,tryAcquireShared()方法直接调用nonfairTryAcquireShared()方法竞争共享锁,代码如下:

final int nonfairTryAcquireShared(int acquires) {
    for (;;) {
        int available = getState();
        int remaining = available - acquires;
        if (remaining < 0 ||
            compareAndSetState(available, remaining))
            return remaining;
    }
}

不管当前AQS的CLH队列中是否有线程排队,对非公平策略来说,直接尝试竞争令牌,有可能再临界点的时候提前抢占到令牌。另外从nonfairTryAcquireShared()方法的实现中发现,所谓的抢占令牌资源,其实就是判断state变量的值。

  • remaining = available - acquires,用当前的令牌数量减去本次需要抢占的令牌数。

    • 如果remaining<0,则说明令牌数量不够,直接返回remaining。

    • 否则就更新state的值,该值表示本次抢占的令牌数量。

  • 返回的remaining如果小于0,则直接让当前线程进入同步队列。

下面的的代码表示公平策略下的竞争令牌的方式,可以发现在通过CAS更新令牌数之前,多个了对hasQueuedPredecessors()方法的判断,该方法的返回结果表示当前同步队列中是否有其他线程在排队,如果有就返回true,这就是FIFO的特性。

static final class FairSync extends Sync {
    private static final long serialVersionUID = 2014338818796000944L;

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

    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;
        }
    }
}

tryAcquireShared()方法返回的只如果小于0,说明令牌数不够,则调用doAcquireSharedInterruptibly()方法将当前线程加入到同步队列中,而同步队列的整个执行过程和上一节的CountDownLatch的执行过程完全一致。

//在Semaphore类中
public void acquire() throws InterruptedException {
    sync.acquireSharedInterruptibly(1);
}
//在AQS中
public final void acquireSharedInterruptibly(int arg)
        throws InterruptedException {
    if (Thread.interrupted())
        throw new InterruptedException();
    if (tryAcquireShared(arg) < 0)
        doAcquireSharedInterruptibly(arg);
}

2.2 释放令牌的过程

通过release()方法释放令牌,本质上是对state字段的值进行累加,代码如下:

public void release() {
    sync.releaseShared(1);
}
#在AQS中
public final boolean releaseShared(int arg) {
    if (tryReleaseShared(arg)) {
        doReleaseShared();
        return true;
    }
    return false;
}
# Semaphore类中
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;
    }
}

我们从上述代码中发现 ,线程每调用一次release()方法就会释放一个令牌,实际上是对state变量的值进行累加,最终通过自旋的方式实现更新过程的原子性。

注意这时候release()方法并没有限制state累加的数量不能超过构造方法限制的permits数量,这意味着通过release()可以扩大令牌的数量,例如初始化时permits数量为5,调用release(1000)使得令牌数量变成1000,只要不超过int类型的整数值(next<current)就不会有问题。

另外,不是必须通过acquire()方法的线程来调用release()方法,任意一个线程都可以调用release()方法来释放令牌。这个是专门为开发者设计的“后门”,可以增加程序的灵活性。

还有就是增加的令牌数 ,可以通过reducePermits()方法进行减少,代码如下:

protected void reducePermits(int reduction) {
    if (reduction < 0) throw new IllegalArgumentException();
    sync.reducePermits(reduction);
}

final void reducePermits(int reductions) {
    for (;;) {
        int current = getState();
        int next = current - reductions;
        if (next > current) // underflow
            throw new Error("Permit count underflow");
        if (compareAndSetState(current, next))
            return;
    }
}

这就意味着,通过release()和reducePermits()两个方法可以动态地对state令牌数实现增加和减少的调整。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

纵横千里,捭阖四方

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值