semaphore源码解析:信号量到底是个啥?

一、概述

semaphore,字面意思是信号量,官方的如下:

 
 * A counting semaphore.  Conceptually, a semaphore maintains a set of
 * permits.  Each {@link #acquire} blocks if necessary until a permit is
 * available, and then takes it.  Each {@link #release} adds a permit,
 * potentially releasing a blocking acquirer.
 * However, no actual permit objects are used; the {@code Semaphore} just
 * keeps a count of the number available and acts accordingly.
 *
 * Semaphores are often used to restrict the number of threads than can
 * access some (physical or logical) resource.

是不是感觉,这解释有点不接地气,又是信号量,又是许可啥的。

下面简单用一个场景来解释,一下子你就可以明白了。


比如说,某个银行大堂,只有三个办事窗口。

这时来了一堆人办业务,都被门口保安给拦住了。

保安说三个窗口,只让三个人进大堂。其它人只能在外边等着。

这时有个人办完业务,出来了。保安这时就放一个人进去。

总之,大堂里,最多只有三个人办理业务。

semaphore 的功能,和这个保安差不多,只让三个人进去。

其实 Semaphore 是 AQS 框架实现的,和 ReentrantLock 实现原理相似。

之前写过一篇《ReentrantLock 的源码分析》,如果看过,这篇就不用细看了。

二、示例代码

下面的代码,创建了 10 个线程,模拟 10 个线程抢 3 个许可的场景。


 public static void main(String[] args) throws Exception {
     Semaphore semaphore = new Semaphore(3,false);  // 创建三个许可
     CountDownLatch downLatch = new CountDownLatch(1);
     for(int i = 0; i < 10; i++){
         new Thread(() -> {
             try {
                 downLatch.await();
                 semaphore.acquire();  // 获取许可
                 int randomSecond = RandomUtil.randomInt(1, 10);
                 Thread.sleep(randomSecond);
                 log.info("当前线程名:{},占用窗口时间:{},准备出来了。", Thread.currentThread().getName(),randomSecond);
                 semaphore.release(); // 解放许可
             } catch (InterruptedException e) {
                 e.printStackTrace();
             }
         }).start();
     }
     Thread.sleep(2000);
     log.info("-------10个线程创建完毕,准备开始抢位置了……");
     downLatch.countDown();

简单解释下代码

Semaphore semaphore = new Semaphore(3,false); 

这句和意思,是创建3个许可,即上面例子中,保安说,只有三个窗口,只让三个人进去。

另: 这里参数 false,影响不大,是公平模式还是非公平模式,后面再具体说。

semaphore.acquire(); 这句指使用一个许可。即上面例子中,某人占用了一个办事窗口。

如果执行这个方法时,许可用完了,即上面例子中,三个窗口都有人,那代码阻塞在这一行。

semaphore.release(); 指许可使用完了,归还许可。即上面例子中,一个人办完业务,出来了。

执行这个方法时,会唤醒被阻塞的线程,使其尝试去申请许可。

前面说过 Semaphore 是 AQS 框架的,结合下面的图,说说 Semaphore的实现原理。

new Semaphore(3,false),即 state 设置为3,

semaphore.acquire(); 有两种情况,

  1. state 大于 0,将 state 的值减 1,结束。(上面例子中,有空窗口,即有许可)
  2. state 小于等于0,进入队列中,等待被唤醒。(上面例子中,没有空窗口,即无许可)

semaphore.release(); 将 state 值加1,并唤醒队列中第一个节点,让其去获取许可。

在这里插入图片描述
原理就这么点,也不复杂,是不是感觉,和 ReentrantLock 差不多呢?

确实差不多,都是有线程竞争时排队,都会限制访问某资源。

ReentrantLock 可以解决高并发问题,Semaphore 能解决高并发问题么?

不一定。new Semaphore(1,false),当把这个参数设置为 1时,跟 ReentrantLock 就一模一样了。

其它情况下,就不一定了。

比如说上面银行大堂的例子,保安是只让三个人进来办业务,

假如第一个窗口,银行前台妹子特别漂亮。

这三个人都抢着去第一个窗口,保安不干涉这个。

即,Semaphore 可以限制资源访问,但不保证原子性。

三、源码解析

源码部分是比较枯燥的,上面解释的差不多可以应对面试了。下面的源码解释可以不用看哈。

  1. 创建实例

Semaphore 类中有个内部类,Sync,该类继承了 AQS

另外公平模式与非公平模式,各自是一个内部类,都继承了 Sync

    abstract static class Sync extends AbstractQueuedSynchronizer {
		……
    }
    
    static final class NonfairSync extends Sync {
		……
    }

    static final class FairSync extends Sync {
		……
    }
  • AQS 本身的属性

private transient Thread exclusiveOwnerThread; // 标识拿到锁的是哪个线程

private transient volatile Node head; // 标识头节点

private transient volatile Node tail; // 标识尾节点

private volatile int state; // 同步状态,为0时,说明可以抢锁
  • AQS 里面的内部类Node的几个属性

volatile int waitStatus;  // Node里,记录状态用的

volatile Thread thread;  // Node里,标识哪个线程

volatile Node prev;  // 前驱节点(这个Node的上一个是谁)

volatile Node next; // 后继节点(这个Node的个一个是谁)

用张图来说明 AQS,大概是这样。

在这里插入图片描述

// 以非公平模式讲解
Semaphore semaphore = new Semaphore(3,false);  

这行代码,对应的源码很简单。在 Semaphore 类中,

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

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

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

不难理解,本例中,创建实例时,AQS中 state 会被设置为 3

  1. acquire()
    总逻辑:
    state 大于 0,将 state 的值减 1,结束。
    state 小于等于0,进入队列中,等待被唤醒。
    public void acquire() throws InterruptedException {
        sync.acquireSharedInterruptibly(1);
    }
    
    public final void acquireSharedInterruptibly(int arg)
            throws InterruptedException {
        if (Thread.interrupted()) // 线程有中断,抛出异常
            throw new InterruptedException();
        if (tryAcquireShared(arg) < 0) // state 状态
            doAcquireSharedInterruptibly(arg); // 入队阻塞
    }
  • 细分逻辑 tryAcquireShared

 protected int tryAcquireShared(int acquires) {
     return nonfairTryAcquireShared(acquires);
 }
 
 final int nonfairTryAcquireShared(int acquires) {
     for (;;) {
         int available = getState(); // 看 state 是多少
         int remaining = available - acquires;
         if (remaining < 0 ||
             compareAndSetState(available, remaining))
             return remaining;
     }
 } 

    protected final int getState() {
        return state;
    }

    protected final boolean compareAndSetState(int expect, int update) {
        // See below for intrinsics setup to support this
        return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
    }    

for (;;) 这个是死循环,高大尚的名字叫自旋。
以上文的例子,10个线程来抢许可。(status = 3, acquires= 1)
同时执行 int remaining = available - acquires;这行,结果是 2
那10个线程都执行 compareAndSetState(available, remaining)) 这行,


这是 CAS 的原子操作,只可能有一个线程执行成功。

该线程将 status 设置为 2return 2( 大于0) acquire 方法结束。

其它线程进入下一次循环。

循环三次之后,将有三个线程抢到许可,status = 0;

进行第四轮循环时,remaining 小于0,直接返回,

tryAcquireShared 返回一个小于0的结果,执行 doAcquireSharedInterruptibly 这个方法

  • 细分逻辑 doAcquireSharedInterruptibly
    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) {
                    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);
        }
    }

这个方法也是一个死循环,首先是入队
要么在循环中抢到许可
要么没抢到,执行 park 方法,即阻塞。

  • addWaiter 这个方法是将节点放入队列中,若队列未初始化,先初始化后再入队。
  • tryAcquireShared 这个方法刚刚讲过了,就是抢许可
  • shouldParkAfterFailedAcquire 判断 Node节点,是不是 -1,不是就设置为-1,是就返回ture;
  • parkAndCheckInterrupt 在上个方法返回true的情况下,执行该方法,执行 park 方法,即阻塞线程,

在《ReentrantLock 源码解析》中详细分析了 以上四个方法的源码,本篇不再分析。
直接输入方法名搜索,就能看到对应的解析。

park 与 unpark 是什么》,如果不清楚,先简单看下这篇。

  1. release 方法

  public void release() {
      sync.releaseShared(1);
  }
  
  public final boolean releaseShared(int arg) {
      if (tryReleaseShared(arg)) {  // status 加 1
          doReleaseShared(); // 唤醒头节点
          return true;
      }
      return false;
  }

相对来说,release 方法简单些,status 减 1, 唤醒节点,即执行 unpark 方法。

  • 细分逻辑 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;
     }
 }

  protected final boolean compareAndSetState(int expect, int update) {
      // See below for intrinsics setup to support this
      return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
  }

tryReleaseShared 这个方法也是一个死循环,直到成功将 status 加 1,返回true。

compareAndSetState 这个方法,是CAS 操作,如果失败,则进行下一次循环。

  • 细分逻辑 doReleaseShared
   private void doReleaseShared() {
        for (;;) {
            Node h = head;
            if (h != null && h != tail) {
                int ws = h.waitStatus;
                if (ws == Node.SIGNAL) {
                    if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                        continue;            // loop to recheck cases
                    unparkSuccessor(h);
                }
                else if (ws == 0 &&
                         !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                    continue;                // loop on failed CAS
            }
            if (h == head)                   // loop if head changed
                break;
        }
    }

doReleaseShared 这个方法,还是死循环,当且仅当 waitStatus = -1时,进行 CAS 操作,将 -1,改这 0 ,不成功,进入下一次循环,成功,则执行 unparkSuccessor

题外话: 什么时候,队列中的 waitStatus是 -1?

当抢许可有竞争时,会有入队操作,shouldParkAfterFailedAcquire 方法会将前驱节点的 waitStatus设置为 -1。

  • 细分逻辑 unparkSuccessor

private void unparkSuccessor(Node node) {
    int ws = node.waitStatus;
    if (ws < 0)
        compareAndSetWaitStatus(node, ws, 0); // 将 waitStatus 设置为 0
    Node s = node.next;
    if (s == null || s.waitStatus > 0) {
        s = null;
        // 从队列尾部开始向前遍历,找到目标节点
        for (Node t = tail; t != null && t != node; t = t.prev)
            if (t.waitStatus <= 0)
                s = t;
    }
    if (s != null)
        LockSupport.unpark(s.thread); // 唤醒目标节点
}

这个方法代码不难理解,最终是唤醒 head 的后继节点。(head 是空节点)

可仔细看下,这里只有唤醒节点,却没有 出队的操作,为什么呢?

不是源码出错,而是写出队操作,很隐蔽。我慢慢说。

在这里插入图片描述

看这张图,LockSupport.unpark(s.thread) 会唤醒 thread=C 的那个 节点。

在讲acquire() 源码时,说过 doAcquireSharedInterruptibly 这个方法,这是一个死循环,最终会在parkAndCheckInterrupt 方法中调用 park 方法,线程被阻塞。

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

thread=C 的那个节点,被唤醒后,再次进入 doAcquireSharedInterruptiblyfor (;;) 循环,
进入 if (p == head) 这个分支


   if (p == head) {
       int r = tryAcquireShared(arg);
       if (r >= 0) {
           setHeadAndPropagate(node, r);
           p.next = null; // help GC
           failed = false;
           return;
       }
   }

    private void setHeadAndPropagate(Node node, int propagate) {
        Node h = head; // Record old head for check below
        setHead(node);
        if (propagate > 0 || h == null || h.waitStatus < 0 ||
            (h = head) == null || h.waitStatus < 0) {
            Node s = node.next;
            if (s == null || s.isShared())
                doReleaseShared();
        }
    }

setHeadAndPropagate 方法会重新设置头节点, p.next = null 这个是旧的头节点出队。

在这里插入图片描述
到这里,release 方法就讲完了。

四、公平与非公平

公平模式与非公平模式,差别不大。

只要是在队列中的节点,只有头节点可以抢许可。这是代码里规定的,没有为什么。

  • 非公平模式:有竞争时,在入队前,有机会同头节点抢许可,若抢失败,就入队。
  • 公平模式:有竞争时,首先入队,只有头节点可抢许可。
    想弄清楚原理,可以参考《公平锁与非公平锁

五、共享锁与排他锁

ReentrantLock 是排他锁,相对应的,Semaphore 是共享锁。

不要被要貌似高大上的名词给忽悠了,其实差别很小,见下图 exclusiveOwnerThread 属性。

在这里插入图片描述
ReentrantLock 加锁实现中,某线程将 state 设置为 1 时,

同时 exclusiveOwnerThread 将指向该线程,即 该锁为该线程独有。

同样解锁,将state 设置为 0 时,同时 exclusiveOwnerThread 设置为 null。

Semaphore 获取许可与释放许可,仅操作 state 这一个属性,即 几个线程共享这个锁。

你看,是不是差别很小,也很明了,弄一个 共享锁-排他锁 的大词儿出来,

瞬间逼格提高了不少,这就是套路!

顺便再说个题外话:ReentrantLock 的可重入性,说简单点,就是每加一次锁,state 就加 1,

同一个线程,可以重复上锁,state 一直往上加。

从这个角度说,Semaphore 也是可重入的,同一个线程,可以连续调用 acquire 方法

  • 4
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值