深入理解Semaphore原理

1. 信号量Semaphore的介绍

我们以一个停车场运作为例来说明信号量的作用。假设停车场只有三个车位,一开始三个车位都是空的。这时如果同时来了三辆车,看门人允许其中它们进入进入,然后放下车拦。以后来的车必须在入口等待,直到停车场中有车辆离开。这时,如果有一辆车离开停车场,看门人得知后,打开车拦,放入一辆,如果又离开一辆,则又可以放入一辆,如此往复。

在这个停车场系统中,车位是公共资源,每辆车好比一个线程,看门人起的就是信号量的作用。信号量是一个非负整数,表示了当前公共资源的可用数目(在上面的例子中可以用空闲的停车位类比信号量),当一个线程要使用公共资源时(在上面的例子中可以用车辆类比线程),首先要查看信号量,如果信号量的值大于1,则将其减1,然后去占有公共资源。如果信号量的值为0,则线程会将自己阻塞,直到有其它线程释放公共资源。

在信号量上我们定义两种操作: acquire(获取) 和 release(释放)。当一个线程调用acquire操作时,它要么通过成功获取信号量(信号量减1),要么一直等下去,直到有线程释放信号量,或超时。release(释放)实际上会将信号量的值加1,然后唤醒等待的线程。

信号量主要用于两个目的,一个是用于多个共享资源的互斥使用,另一个用于并发线程数的控制。

在学习Semaphore之前我们可以先对同步器进行学习,这样有助于我们学习Semaphore
传送门:

深入理解AQS(AbstractQueuedSynchronizer)

深入理解CountDownLatch原理

深入理解ReentrantLock原理

深入理解CyclicBarrier原理

2.源码分析

   2.1Semaphore的内部类

  Semaphore总共有三个内部类,并且三个内部类是紧密相关的,下面先看三个类的关系。

  说明:Semaphore与ReentrantLock的内部类的结构相同,类内部总共存在Sync、NonfairSync、FairSync三个类,NonfairSync与FairSync类继承自Sync类,Sync类继承自AbstractQueuedSynchronizer抽象类。下面逐个进行分析。

    1).Sync类   

  Sync类的源码如下:

// 内部类,继承自AQS
    abstract static class Sync extends AbstractQueuedSynchronizer {
        // 版本号
        private static final long serialVersionUID = 1192457210091910933L;
        
        // 构造函数
        Sync(int permits) {
            // 设置状态数
            setState(permits);
        }
        
        // 获取许可
        final int getPermits() {
            return getState();
        }

        // 共享模式下非公平策略获取
        final int nonfairTryAcquireShared(int acquires) {
            for (;;) { // 无限循环
                // 获取许可数
                int available = getState();
                // 剩余的许可
                int remaining = available - acquires;
                if (remaining < 0 ||
                    compareAndSetState(available, remaining)) // 许可小于0或者比较并且设置状态成功
                    return remaining;
            }
        }
        
        // 共享模式下进行释放
        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;
            }
        }

        // 根据指定的缩减量减小可用许可的数目
        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;
            }
        }

        // 获取并返回立即可用的所有许可
        final int drainPermits() {
            for (;;) { // 无限循环
                // 获取许可
                int current = getState();
                if (current == 0 || compareAndSetState(current, 0)) // 许可为0或者比较并设置成功
                    return current;
            }
        }
    }

    说明:Sync类的属性相对简单,只有一个版本号,Sync类存在如下方法和作用如下:

    

  2). NonfairSync类

  NonfairSync类继承了Sync类,表示采用非公平策略获取资源,其只有一个tryAcquireShared方法,重写了AQS的该方法,其源码如下:

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方法的源码可知,其会调用父类Sync的nonfairTryAcquireShared方法,表示按照非公平策略进行资源的获取。

  3). FairSync类

  FairSync类继承了Sync类,表示采用公平策略获取资源,其只有一个tryAcquireShared方法,重写了AQS的该方法,其源码如下:  

protected int tryAcquireShared(int acquires) {
            for (;;) { // 无限循环
                if (hasQueuedPredecessors()) // 同步队列中是否存在其他节点
                    return -1;
                // 获取许可
                int available = getState();
                // 剩余的许可
                int remaining = available - acquires;
                if (remaining < 0 ||
                    compareAndSetState(available, remaining)) // 剩余的许可小于0或者比较设置成功
                    return remaining;
            }
        }

说明:从tryAcquireShared方法的源码可知,它使用公平策略来获取资源,它会判断同步队列中是否存在其他的等待节点。

    2.2类的属性和构造函数

    public class Semaphore implements java.io.Serializable {
        // 版本号
        private static final long serialVersionUID = -3222578661600680210L;
        // 属性
        private final Sync sync;
    }

    //说明:Semaphore自身只有两个属性,最重要的是sync属性,
    基于Semaphore对象的操作绝大多数都转移到了对sync的操作。

    public Semaphore(int permits) {
        sync = new NonfairSync(permits);
    }
    //说明:该构造函数会创建具有给定的许可数和非公平的公平设置的Semaphore

    public Semaphore(int permits, boolean fair) {
        sync = fair ? new FairSync(permits) : new NonfairSync(permits);
    }

    //说明:该构造函数会创建具有给定的许可数和给定的公平设置的Semaphore。

   2.3acquire函数--获取许可

  先从获取一个许可看起,并且先看非公平模式下的实现。首先看acquire方法,acquire方法有几个重载,但主要是下面这个方法

    public void acquire(int permits) throws InterruptedException {
        if (permits < 0) throw new IllegalArgumentException();
        sync.acquireSharedInterruptibly(permits);
    }

 从上面可以看到,调用了Sync的acquireSharedInterruptibly方法,该方法在父类AQS中,如下:

    public final void acquireSharedInterruptibly(int arg)
            throws InterruptedException {
        //如果线程被中断了,抛出异常
        if (Thread.interrupted())
            throw new InterruptedException();
        //获取许可失败,将线程加入到等待队列中
        if (tryAcquireShared(arg) < 0)
            doAcquireSharedInterruptibly(arg);
    }

AQS子类如果要使用共享模式的话,需要实现tryAcquireShared方法,下面看NonfairSync的该方法实现:

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

该方法调用了父类中的nonfairTyAcquireShared方法,如下:

        final int nonfairTryAcquireShared(int acquires) {
            for (;;) {
                //获取剩余许可数量
                int available = getState();
                //计算给完这次许可数量后的个数
                int remaining = available - acquires;
                //如果许可不够或者可以将许可数量重置的话,返回
                if (remaining < 0 ||
                    compareAndSetState(available, remaining))
                    return remaining;
            }
        }

这里的释放就是对 state 变量减一(或者更多)的。

返回了剩余的 state 大小。

当返回值小于 0 的时候,说明获取锁失败了,那么就需要进入 AQS 的等待队列了。代码如下:

private void doAcquireSharedInterruptibly(int arg)
    throws InterruptedException {
    // 添加一个节点 AQS 队列尾部
    final Node node = addWaiter(Node.SHARED);
    boolean failed = true;
    try {
        // 死循环
        for (;;) {
            // 找到新节点的上一个节点
            final Node p = node.predecessor();
            // 如果这个节点是 head,就尝试获取锁
            if (p == head) {
                // 继续尝试获取锁,这个方法是子类实现的
                int r = tryAcquireShared(arg);
                // 如果大于0,说明拿到锁了。
                if (r >= 0) {
                    // 将 node 设置为 head 节点
                    // 如果大于0,就说明还有机会获取锁,那就唤醒后面的线程,称之为传播
                    setHeadAndPropagate(node, r);
                    p.next = null; // help GC
                    failed = false;
                    return;
                }
            }
            // 如果他的上一个节点不是 head,就不能获取锁
            // 对节点进行检查和更新状态,如果线程应该阻塞,返回 true。
            if (shouldParkAfterFailedAcquire(p, node) &&
                // 阻塞 park,并返回是否中断,中断则抛出异常
                parkAndCheckInterrupt())
                throw new InterruptedException();
        }
    } finally {
        if (failed)
            // 取消节点
            cancelAcquire(node);
    }
}

总的逻辑就是:

  1. 创建一个分享类型的 node 节点包装当前线程追加到 AQS 队列的尾部。
  2. 如果这个节点的上一个节点是 head ,就是尝试获取锁,获取锁的方法就是子类重写的方法。如果获取成功了,就将刚刚的那个节点设置成 head。
  3. 如果没抢到锁,就阻塞等待。

看完了非公平的获取,再看下公平的获取,代码如下:

        protected int tryAcquireShared(int acquires) {
            for (;;) {
                //如果前面有线程再等待,直接返回-1
                if (hasQueuedPredecessors())
                    return -1;
                //后面与非公平一样
                int available = getState();
                int remaining = available - acquires;
                if (remaining < 0 ||
                    compareAndSetState(available, remaining))
                    return remaining;
            }
        }

从上面可以看到,FairSync与NonFairSync的区别就在于会首先判断当前队列中有没有线程在等待,如果有,就老老实实进入到等待队列;而不像NonfairSync一样首先试一把,说不定就恰好获得了一个许可,这样就可以插队了。
看完了获取许可后,再看一下release()方法。

   2.4release()函数--释放许可

 释放许可也有几个重载方法,但都会调用下面这个带参数的方法

    public void release(int permits) {
        if (permits < 0) throw new IllegalArgumentException();
        sync.releaseShared(permits);
    }

releaseShared方法在AQS中,如下:

    public final boolean releaseShared(int arg) {
        //如果改变许可数量成功
        if (tryReleaseShared(arg)) {
            doReleaseShared();
            return true;
        }
        return false;
    }

AQS子类实现共享模式的类需要实现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");
                //CAS改变许可数量成功,返回true
                if (compareAndSetState(current, next))
                    return true;
            }
        }

从上面可以看到,一旦CAS改变许可数量成功,那么就会调用doReleaseShared()方法释放阻塞的线程

private void doReleaseShared() {
    for (;;) {
        Node h = head;
        if (h != null && h != tail) {
            int ws = h.waitStatus;
            if (ws == Node.SIGNAL) {
                // 设置 head 的等待状态为 0 ,并唤醒 head 上的线程
                if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                    continue;            // loop to recheck cases
                unparkSuccessor(h);
            }
            // 成功设置成 0 之后,将 head 状态设置成传播状态
            else if (ws == 0 &&
                     !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                continue;                // loop on failed CAS
        }
        if (h == head)                   // loop if head changed
            break;
    }
}

该方法的主要作用就是从 AQS 的 head 节点开始唤醒线程,注意,这里唤醒是 head 节点的下一个节点,需要和 doAcquireSharedInterruptibly方法对应,因为 doAcquireSharedInterruptibly 方法唤醒的当前节点的上一个节点,也就是 head 节点。至此,释放 state 变量,唤醒 AQS 头节点结束。

   2.5reducePermits--减少许可

   Semaphore还有减小许可数量的方法,该方法可以用于用于当资源用完不能再用时,这时就可以减小许可证。代码如下:

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

  可以看到,委托给了Sync,Sync的reducePermits方法如下:

      final void reducePermits(int reductions) {
            for (;;) {
                //得到当前剩余许可数量
                int current = getState();
                //得到减完之后的许可数量
                int next = current - reductions;
                if (next > current) // underflow
                    throw new Error("Permit count underflow");
                //如果CAS改变成功
                if (compareAndSetState(current, next))
                    return;
            }
        }

  从上面可以看到,就是CAS改变AQS中的state变量,因为该变量代表许可证的数量。

   2.6drainPermits--耗尽许可

    public int drainPermits() {
        return sync.drainPermits();
    }

     final int drainPermits() {
            for (;;) {
                int current = getState();
                if (current == 0 || compareAndSetState(current, 0))
                    return current;
            }
      }

可以看到,就是CAS将许可数量置为0。

3.示例使用

Semaphore代码并没有很复杂,常用的操作就是获取和释放一个许可证,这些操作的实现逻辑也都比较简单,但这并不妨碍Semaphore的广泛应用。下面我们就来利用Semaphore实现一个简单的数据库连接池,通过这个例子希望读者们能更加深入的掌握Semaphore的运用。

public class ConnectPool {
    
    //连接池大小
    private int size;
    //数据库连接集合
    private Connect[] connects;
    //连接状态标志
    private boolean[] connectFlag;
    //剩余可用连接数
    private volatile int available;
    //信号量
    private Semaphore semaphore;
    
    //构造器
    public ConnectPool(int size) {  
        this.size = size;
        this.available = size;
        semaphore = new Semaphore(size, true);
        connects = new Connect[size];
        connectFlag = new boolean[size];
        initConnects();
    }
    
    //初始化连接
    private void initConnects() {
        //生成指定数量的数据库连接
        for(int i = 0; i < this.size; i++) {
            connects[i] = new Connect();
        }
    }
    
    //获取数据库连接
    private synchronized Connect getConnect(){  
        for(int i = 0; i < connectFlag.length; i++) {
            //遍历集合找到未使用的连接
            if(!connectFlag[i]) {
                //将连接设置为使用中
                connectFlag[i] = true;
                //可用连接数减1
                available--;
                System.out.println("【"+Thread.currentThread().getName()+"】以获取连接      剩余连接数:" + available);
                //返回连接引用
                return connects[i];
            }
        }
        return null;
    }
    
    //获取一个连接
    public Connect openConnect() throws InterruptedException {
        //获取许可证
        semaphore.acquire();
        //获取数据库连接
        return getConnect();
    }
    
    //释放一个连接
    public synchronized void release(Connect connect) {  
        for(int i = 0; i < this.size; i++) {
            if(connect == connects[i]){
                //将连接设置为未使用
                connectFlag[i] = false;
                //可用连接数加1
                available++;
                System.out.println("【"+Thread.currentThread().getName()+"】以释放连接      剩余连接数:" + available);
                //释放许可证
                semaphore.release();
            }
        }
    }
    
    //剩余可用连接数
    public int available() {
        return available;
    }
    
}

测试代码: 

public class TestThread extends Thread {
    
    private static ConnectPool pool = new ConnectPool(3);
    
    @Override
    public void run() {
        try {
            Connect connect = pool.openConnect();
            Thread.sleep(100);  //休息一下
            pool.release(connect);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    
    public static void main(String[] args) {
        for(int i = 0; i < 10; i++) {
            new TestThread().start();
        }
    }

}

测试结果:

我们使用一个数组来存放数据库连接的引用,在初始化连接池的时候会调用initConnects方法创建指定数量的数据库连接,并将它们的引用存放到数组中,此外还有一个相同大小的数组来记录连接是否可用。每当外部线程请求获取一个连接时,首先调用semaphore.acquire()方法获取一个许可证,然后将连接状态设置为使用中,最后返回该连接的引用。许可证的数量由构造时传入的参数决定,每调用一次semaphore.acquire()方法许可证数量减1,当数量减为0时说明已经没有连接可以使用了,这时如果其他线程再来获取就会被阻塞。每当线程释放一个连接的时候会调用semaphore.release()将许可证释放,此时许可证的总量又会增加,代表可用的连接数增加了,那么之前被阻塞的线程将会醒来继续获取连接,这时再次获取就能够成功获取连接了。测试示例中初始化了一个3个连接的连接池,我们从测试结果中可以看到,每当线程获取一个连接剩余的连接数将会减1,等到减为0时其他线程就不能再获取了,此时必须等待一个线程将连接释放之后才能继续获取。可以看到剩余连接数总是在0到3之间变动,说明我们这次的测试是成功的。

4.总结

Semaphore是信号量,用于管理一组资源。其内部是基于AQS的共享模式,AQS的状态表示许可证的数量,在许可证数量不够时,线程将会被挂起;而一旦有一个线程释放一个资源,那么就有可能重新唤醒等待队列中的线程继续执行。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值