Semaphore源码解析与应用详解

86 阅读7分钟

是什么?用来干什么的?

Semaphore通常我们叫做信号量,可以用来控制同时访问特定资源的线程数量,通过协调各个线程,以保证合理的使用资源

举个简单的例子: 一个单向隧道能同时容纳10个小汽车或5个卡车通过(1个卡车等效与2个小汽车), 而隧道入口记录着当前已经在隧道内的汽车等效比重. 比如1个小汽车和1个卡车, 则隧道入口显示3. 若隧道入口显示10表示已经满了. 当汽车驶出隧道之后, 隧道入口显示的数字则会相应的减小. 于这个示例相符合场景非常适合用信号量

Semaphore在构造的时候, 可以传入一个int. 表示有多少许可(permit). 线程获取锁的时候, 要告诉信号量使用多少许可(类比与小汽车和卡车), 当线程要使用的许可不足时, 则调用的线程则会被阻塞. 可以和上面简单的举例进行初步理解.

使用场景:

通常用于哪些资源有明确访问数量限制的场景,常用于限流:

  • 数据库连接池,同时进行连接的线程有数量限制,连接不能超过一定的数量,当连接达到了限制的数量后,后面的线程只能排队等待前面的线程释放了数据库连接才能获得数据库连接
  • 再停车场的场景下,车位数量是有限的,同时能容纳多少台车是由数量要求的,车位满了之后,后面来的车如果想要进入这个停车场,只有等待里面的车离开停车场,有位置后才可以进入。

基本使用:

常用方法:

acquire()  
获取一个令牌,在获取到令牌、或者被其他线程调用中断之前线程一直处于阻塞状态。
acquire(int permits)  
获取一个令牌,在获取到令牌、或者被其他线程调用中断、或超时之前线程一直处于阻塞状态。
acquireUninterruptibly() 
获取一个令牌,在获取到令牌之前线程一直处于阻塞状态(忽略中断)。 
tryAcquire()
尝试获得令牌,返回获取令牌成功或失败,不阻塞线程。
tryAcquire(long timeout, TimeUnit unit)
尝试获得令牌,在超时时间内循环尝试获取,直到尝试获取成功或超时返回,不阻塞线程。
release()
释放一个令牌,唤醒一个获取令牌不成功的阻塞线程。
hasQueuedThreads()
等待队列里是否还存在等待线程。
getQueueLength()
获取等待队列里阻塞的线程数。
drainPermits()
清空令牌把可用令牌数置为0,返回清空令牌的数量。
availablePermits()
返回可用的令牌数量。

用semaphore 实现停车场提示牌功能。

每个停车场入口都有一个提示牌,上面显示着停车场的剩余车位还有多少,当剩余车位为0时,不允许车辆进入停车场,直到停车场里面有车离开停车场,这时提示牌上会显示新的剩余车位数。

package 循环阑珊.信号量;
​
import sun.security.util.AuthResources_it;
​
import java.util.Random;
import java.util.concurrent.Semaphore;
​
public class Demo11 {
    public static void main(String[] args) {
        //设置停车场的大小为10
        Semaphore semaphore=new Semaphore(10);
        for (int i = 1; i <=100 ; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    System.out.println("==="+Thread.currentThread().getName()+"来到停车场");
                    //判断是否还有车位
                    if(semaphore.availablePermits()==0){
                        System.out.println("没有车位了,请耐心等待一下");
                    }
                    //尝试获取一个令牌看现在是否有车位了
                    try {
                        semaphore.acquire();
                        System.out.println(Thread.currentThread().getName()+"成功进入到停车场");
                        //模拟车在停车场所停留的时间
                        Thread.sleep(new Random().nextInt(10000));
                        //走出停车场
                        System.out.println(Thread.currentThread().getName()+"离开了停车场");
                        //归还令牌,腾出空的位置,让其他车辆可以获取令牌进入到停车场里面
                        semaphore.release();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            },i+"号").start();
        }
    }
}

Semaphore实现原理

Semaphore初始化。

Semaphore semaphore=new Semaphore(2);
public Semaphore(int permits) {
    sync = new NonfairSync(permits);
}
  • 当调用new Semaphore(2)方法时,默认是会创建一个非公平的锁的同步阻塞队列
  • 把初始令牌数量赋值给同步队列的state状态,state的值就代表当前所剩余的令牌的数量

获取令牌

semaphore.acquire();
  • 当前线程会尝试去同步队列获取一个令牌,获取令牌的过程也就是使用原子的操作去修改同步队列中的state,获取一个令牌后state就会state=state-1
  • 当计算出来的state<=0,那么久代表当前令牌的数量不够了,就会创建一个Node结点加入到阻塞队列中,挂起当前线程
  • 当计算出来的state>0,那么就代表获取令牌成功了
public void acquire() throws InterruptedException {
    //获取一个令牌
    sync.acquireSharedInterruptibly(1);
}
//共享模式下获取令牌,获取成功就返回,失败就加入到阻塞队列,挂起线程
public final void acquireSharedInterruptibly(int arg)
        throws InterruptedException {
    if (Thread.interrupted())
        //如果线程中断,就报中断异常
        throw new InterruptedException();
    //尝试获取令牌,arg为获取令牌的个数,当可用的令牌数量减去当前令牌数量结果小于0,就生成一个node结点加入到阻塞队列中,将线程挂起
    if (tryAcquireShared(arg) < 0)
        //为常见结点加入阻塞队列的方法
        doAcquireSharedInterruptibly(arg);
}
/**
     * 1、创建节点,加入阻塞队列,
     * 2、重双向链表的head,tail节点关系,清空无效节点
     * 3、挂起当前节点线程
     * @param arg
     * @throws InterruptedException
     */
    private void doAcquireSharedInterruptibly(int arg)
        throws InterruptedException {
        //创建节点加入阻塞队列
        final Node node = addWaiter(Node.SHARED);
        boolean failed = true;
        try {
            for (;;) {
                //获得当前节点pre节点
                final Node p = node.predecessor();
                // 前任是头节点, 则尝试去获令牌
                if (p == head) {
                    int r = tryAcquireShared(arg);//返回锁的state
                    // 获取令牌成功,设置头节点,并且进行传播
                    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);
        }
    }

注意: 若获取锁失败,关键方法doAcquireSharedInterruptibly阻塞的获取锁.

  1. 加到双向链表
  2. 是头节点后继, 则尝试获取锁, 否者则判断进入睡眠等待唤醒, 唤醒后继续执行获取令牌
  3. 不进入睡眠,则直接运行到获取令牌

释放令牌

 semaphore.release();

当调用该 semaphore.release();方法的时候:

  • 线程会尝试释放一个令牌,释放令牌的过程也就是把同步队列的state修改为state=state+1的过程
  • 释放令牌成功之后,同时会唤醒同步队列中的一个线程
  • 被唤醒的结点会重新尝试去修改state=state-1的操作,如果state>=0则获取令牌成功,否则重新进入阻塞队列,挂起线程
 /**
     * 释放令牌
     */
    public void release() {
        sync.releaseShared(1);
    }
/**
     *释放共享锁,同时会唤醒同步队列中的一个线程。
     * @param arg
     * @return
     */
    public final boolean releaseShared(int arg) {
        //释放共享锁
        if (tryReleaseShared(arg)) {
            //// tryReleaseShared释放成功, 则释放双向链表中head的后继
            doReleaseShared();
            return true;
        }
        return false;
    }
// Semaphore#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;
    }
}
//唤醒阻塞队列中的一个线程
private void doReleaseShared() {
    for (;;) {
        // 记录当前head
        Node h = head;
        // 队列中含有等待的节点
        if (h != null && h != tail) {
            // 记录头节点等待状态
            int ws = h.waitStatus;
            // 有下一个节点需要唤醒
            if (ws == Node.SIGNAL) {
                // CAS 设置状态, 若没有成功, 则是并发导致失败.
                if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                    continue;
                // 唤醒后继.
                unparkSuccessor(h);
            }
            // 并发情况下,可能会出现wa为0,需要状态为PROPAGATE,保证唤醒
            else if (ws == 0 &&
                     !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                continue;
        }
        if (h == head)
            break;
    }
}

Semaphore释放锁的过程总结为如下:

  1. 释放N个许可, 因为存在并发释放, 需要CAS确保设置更新后的值.
  2. 唤醒双向链表中有效的等待节点. (可能存在并发问题,引入PROPAGATE状态)
  3. 被唤醒的节点调用获取令牌的流程.