Semaphore实现原理

77 阅读3分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第23天,点击查看活动详情

一、Semaphore初始化。

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

二、获取令牌

semaphore.acquire();
  • 判断是否满足获取锁条件, 关键方法nonfairTryAcquireShared()
  • 若获取锁成功,则也会修改state
  • 若获取锁失败,关键方法doAcquireSharedInterruptibly()阻塞的获取锁

本质上是调用的AQS#acquireSharedInterruptibly(int), 参数为1

1、当前线程会尝试去同步队列获取一个令牌,获取令牌的过程也就是使用原子的操作去修改同步队列的state ,获取一个令牌则修改为state = state-1。

2、 当计算出来的state<0,则代表令牌数量不足,此时会创建一个Node节点加入阻塞队列,挂起当前线程。

3、当计算出来的state>=0,则代表获取令牌成功。

/**
     *  获取1个令牌
     */
    public void acquire() throws InterruptedException {
        sync.acquireSharedInterruptibly(1);
    }
/**
     * 共享模式下获取令牌,获取成功则返回,失败则加入阻塞队列,挂起线程
     * @param arg
     * @throws InterruptedException
     */
    public final void acquireSharedInterruptibly(int arg)
            throws InterruptedException {
        if (Thread.interrupted())
            throw new InterruptedException();
        //尝试获取令牌,arg为获取令牌个数,当可用令牌数减当前令牌数结果小于0,则创建一个节点加入阻塞队列,挂起当前线程。
        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);
        }
    }

三、释放令牌

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

两个方法都会调用AQS#releaseShared(int)方法, 使用release()方法,则参数为1, 使用release(int)方法, 则参数为int。

当调用semaphore.release() 方法:

1、线程会尝试释放一个令牌,释放令牌的过程也就是把同步队列的state修改为state=state+1的过程 2、释放令牌成功之后,同时会唤醒同步队列中的一个线程。 3、被唤醒的节点会重新尝试去修改state=state-1 的操作,如果state>=0则获取令牌成功,否则重新进入阻塞队列,挂起线程。

 /**
     * 释放令牌
     */
    public void release() {
        sync.releaseShared(1);
    }

/**
     *释放共享锁,同时会唤醒同步队列中的一个线程。
     * @param arg
     * @return
     */
    public final boolean releaseShared(int arg) {
        //释放共享锁
        if (tryReleaseShared(arg)) {
            //唤醒所有共享节点线程
            doReleaseShared();
            return true;
        }
        return false;
    }


/**
     * 唤醒同步队列中的一个线程
     */
    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))//修改状态为初始0
                        continue;
                    unparkSuccessor(h);//唤醒h.nex节点线程
                }
                else if (ws == 0 &&
                         !compareAndSetWaitStatus(h, 0, Node.PROPAGATE));
            }
            if (h == head)                   // loop if head changed
                break;
        }
    }