【并发编程】(十八)信号量Semaphore使用及实现原理

1.SemaPhore概述

在之前的文中中提到了并发编程使用到的锁,不管是synchronized还是ReentrantLock,其目的都是为了限制线程对资源的访问,从而起到保证线程安全的作用。
信号量SemaPhore也是在限制多个线程访问共享资源,它与锁(这里指互斥锁)的区别在于,锁限制了只能有一个线程可以访问到共享资源,而信号量可以根据需要来配置多个线程访问共享资源,例如配置的信号量为10,则有10个线程可以在临界区内活跃,如果将信号量配置为1,也可以到达和互斥锁类似的相关。

1.1.共享锁与独占锁

信号量Semaphore实际上是一个共享锁,它与独占锁的区别也比较容易理解,顾名思义,独占锁就是只有一个线程可以独占的锁,而共享锁可以让多个线程获取。Semaphore是通过AQS的共享模式来实现的。

1.2.Semaphore的作用

Semaphore最主要的作用就是用来做流量控制,也就是限流。
在《Java并发编程的艺术》中写到一个数据库连接的例子,我们需要从数据库中读取几万个文件的数据,对于这种IO密集型的操作,我们可以启动几十个线程做一个并发的操作来加快读取效率,但是数据库的连接只有10个,为了避免出现无法获取数据库连接的异常,我们就可以使用Semaphore限制访问的线程数量为10个,达到限流的效果。

1.3.Semaphore的使用

Semaphore的基本使用非常简单,就是使用acquire()release()方法将需要限制线程数量的代码逻辑包裹起来就可以了,平时使用这两个方法就已经足够了,下面写了个简单的Demo来验证对线程数量的限制是否生效:

public class SemaphoreDemo {

    public static void test(Semaphore semaphore) {
        try {
            semaphore.acquire();
            System.out.println(Thread.currentThread().getName()+":enter");
            Thread.sleep(1000L);
            System.out.println(Thread.currentThread().getName()+":exit");
            semaphore.release();

        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        Semaphore semaphore = new Semaphore(2);
        for (int i = 0; i < 6; i++) {
            new Thread(() -> SemaphoreDemo.test(semaphore), "线程" + i).start();
        }
    }
}

当前的信号量为2,那预估的结果一定是有两个线程打印出enter后,至少有一个线程打印出exit,才会有新的线程打印出enter。
实际打印结果如下:

线程0:enter
线程1:enter
线程1:exit
线程0:exit
线程2:enter
线程5:enter
线程5:exit
线程2:exit
线程4:enter
线程3:enter
线程4:exit
线程3:exit

可以看到线程访问的顺序虽然是乱序的,但一定是有线程退出了临界区才会有新的线程进入临界区。

2.Semaphore的实现原理

Semaphore是通过AQS的共享模式实现的,它使用了state变量来限制进入临界区的线程数量,每个线程调用了acquire()方法就表示进入了临界区,此时state递减1,在state减为0后,就不会再有新的线程进入临界区了,然后执行完临界区的代码逻辑后,调用release()方法此时state递增1。

2.1.Semaphore实例化

和其它使用AQS实现的JUC工具类一样,Semaphore也有一个叫Sync的内部类继承了AQS,同时它有两个子类分别对应公平锁和非公平锁,在之前的文章中提到了,公平锁和非公平锁区别只在于新的线程获取锁时是否会做一次抢锁的操作,如果是就是非公平锁。在这里就以非公平锁来分析Semaphore的实现原理。

Semaphore通过构造方法进行实例化:

// 非公平锁
public Semaphore(int permits) {
    sync = new NonfairSync(permits);
}

// 传入true则为公平锁
public Semaphore(int permits, boolean fair) {
    sync = fair ? new FairSync(permits) : new NonfairSync(permits);
}

permits翻译过来是许可的意思,这里创建Semaphore实例时,就会指定一定数量的“许可证”,供后续使用。然后我们可以继续往里面看,看看permits到底是什么东西。

abstract static class Sync extends AbstractQueuedSynchronizer {
    Sync(int permits) {
        setState(permits);
    }
}

public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer implements java.io.Serializable{
	
	private volatile int state;

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

从上面的代码可以看出来,permits就是AQS中的state

2.2.Semaphore申请许可——acquire()

在实例化Semaphore的时候创建了一定数量的“许可证”,线程只有在获取到“许可证”时才能进入到临界区执行代码,我们看看acquire()里面做了什么。

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

里面调用了一个AQS共享模式的通用方法,这里调用的是可以被中断等待的资源获取方法,在Semaphore中还有另外一个不可中断的方法:

public void acquireUninterruptibly() {
    sync.acquireShared(1);
}

但不管是调用哪个方法,我们忽略掉是否可以中断的逻辑,它们都会去调用AQS的模板方法tryAcquireShared

public final void acquireSharedInterruptibly(int arg) throws InterruptedException {
    if (Thread.interrupted())
        throw new InterruptedException();
    // 这一步是重点
    if (tryAcquireShared(arg) < 0)
        doAcquireSharedInterruptibly(arg);
}

这个模板方法是交给子类去实现的,不同的子类可以定义对state值的不同操作方式,这里我们点进去,选择Semaphore的非公平锁实现,最终实现如下:

final int nonfairTryAcquireShared(int acquires) {
	// 无限循环+CAS ,典型的自旋锁实现
    for (;;) {
        int available = getState();
        int remaining = available - acquires;
        // remaning < 0 表示所有的“许可证”都被发完了
        if (remaining < 0 ||
            compareAndSetState(available, remaining))
            return remaining;
    }
}

每个进入这个方法的线程都会使用CAS将当前的state减去一定的数量,如果是调用的acquire()这里就是减1,在替换成功后或计算值减到-1后就会返回当前计算的状态。
这个返回值会和0做比较if (tryAcquireShared(arg) < 0),如果判断为false,当前线程就成功的进入临界区执行代码逻辑了。如果判断为true,则会调用AQS的通用方法,将线程放入到队列中挂起。

private void doAcquireSharedInterruptibly(int arg)
    throws InterruptedException {
    // 新增阻塞队列节点
    final Node node = addWaiter(Node.SHARED);
    boolean failed = true;
    try {
        for (;;) {
            final Node p = node.predecessor();
            if (p == head) {	
            	// 被唤醒后去抢锁,这个方法是模板方法,选择Semaphore的实现
                int r = tryAcquireShared(arg);
                if (r >= 0) {
                    setHeadAndPropagate(node, r);
                    p.next = null; // help GC
                    failed = false;
                    return;
                }
            }
            // 挂起
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                throw new InterruptedException();
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

看过之前CountdownLatch的实现原理的同学,对这个代码可能会比较眼熟,这里就是AQS的“三板斧”。

  • 将当前线程包装成一个Node节点,加入到CLH队列尾部。
  • 修改前置节点状态为signal并将当前线程挂起。
  • 被唤醒后,再次通过自己实现的tryAcquireShared()判断是否获取锁,获取锁后就退出自旋执行临界区代码,没获取到就再次挂起。

2.3.Semaphore归还许可——release()

acquire()一直,release()也是调用的AQS的公共方法:

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

doReleaseShared()中,就是将CLH中挂起的线程依次唤醒,让他们去抢锁,这个在之前的文中《(十六)栅栏CountDownLatch的使用及实现原理》2.2.线程唤醒的实现中做了分析,由于事先原理一致,这里就不做详细分析了。
调用doReleaseShared()的条件就是tryReleaseShared(arg)的返回值,这也是一个模板方法,我们去找到Semaphore的实现:

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

上面代码可以看出,在CAS替换成功,也就是“许可令”成功归还后,就可以去唤醒CLH中挂起的线程去竞争“许可令”,从而进入到临界区执行代码。

3.总结

Semaphore是用来限制访问共享资源的线程数量的工具,它底层通过AQS的特性来实现的。

  • 使用AQSstate字段值来限制线程数量。
  • 使用CASstate的递增和递减做原子性操作。
  • 对于超出限制数量的线程,将它们加入到CLH队列中,防止忙等现象。
  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

挥之以墨

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

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

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

打赏作者

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

抵扣说明:

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

余额充值