信号量,semaphore源代码之我见

信号量,Semaphore,一个限定访问线程数量的工具类,属于并发包java.util.concurrent 里面的类。

Semaphore,内部提供了构造方法(包含默认的非公平信号量构造方法,已经可设置是否公平的构造方法)、获取信号量acquire()、尝试获取信号量tryAcquire()、

规定时间内尝试获取信号量tryAcquire(long timeout, TimeUnit unit)、

释放信号量release()、获取可用的信号量数量availablePermits()、重置信号drainPermits()、叠减信号量reducePermits(int reduction)等方法。其中,获取信号量、释放信

号量,均有多个重载,区别于是否抛出异常、是否有时间限制、获取及释放信号量的数量。

其中,最重要的是获取信号量acquire() 以及释放信号量 release()

 ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- 

Semaphore和可重入锁ReenTrantLock很相似,都有一个内部类Sync。这个内部类Sync继承了AbstractQueuedSynchronizer(AQS),除了父类AQS的原有方法外,还

构造方法Sync(int permits)、获取信号量getPermits()、尝试获取信号量final int nonfairTryAcquireShared(int acquires)、

尝试释放信号量tryReleaseShared(int releases)、叠减信号量reducePermits(int reductions)、信号量归零(重置)drainPermits() 几个方法。

Sync类,有两个子类,非公平信号量NonfairSync  以及 公平信号量FairSync。两个子类,都有两个方法:构造方法、以及从超类里继承的抽象方法

int tryAcquireShared(int acquires)。

----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

先看构造方法Semaphore(int permits)。源代码如下:

 public Semaphore(int permits) {
        sync = new NonfairSync(permits);
    }

从上面可以看出,信号量默认使用的是非公平信号量。这个是符合计算机的最大计算机资源使用率的思想的。

获取信号量acquire(),源代码如下:

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) //尝试获取信号量,如果失败,表示信号量已经被获取完了。这个tryAcquireShared从AQS继承是由Sync的子类实现
           doAcquireSharedInterruptibly(arg); }   //以共享可中断模式获取信号量
//尝试获取公平信号量
protected
int tryAcquireShared(int acquires) { for (;;) { //死循环,表现在运行期间,就是阻塞。也就是说,所谓的运行期间阻塞,在代码里,本质上是执行了死循环。 if (hasQueuedPredecessors()) //因为公平信号量,始终是从第一个节点开始运行的。所以,如果没有前驱节点,直接返回-1,失败 return -1; int available = getState(); //获取信号量的数量。从这里也可以看出,信号量的数量,是放在AQS的由volatile修饰state字段的。 int remaining = available - acquires; if (remaining < 0 || //如果信号量 <0,直接返回信号量的剩余数量。否则,执行CAS操作,设置剩余信号量 compareAndSetState(available, remaining)) return remaining; } }
protected int tryAcquireShared(int acquires) {
            return nonfairTryAcquireShared(acquires);
        }

final int nonfairTryAcquireShared(int acquires) {
            for (;;) {
                int available = getState();
                int remaining = available - acquires;
                if (remaining < 0 ||
                    compareAndSetState(available, remaining))
                    return remaining;
            }
        }

从上面可以看出,公平信号量和非公平信号量,代码大同小异。区别在于,公平信号信号量,总是从头节点开始的。

下面看 以共享可中断模式获取信号量doAcquireSharedInterruptibly(arg)

/**
     * Acquires in shared interruptible mode.
     * @param arg the acquire argument
     */
    private void doAcquireSharedInterruptibly(int arg)
        throws InterruptedException {
        final Node node = addWaiter(Node.SHARED);   //增加waiter,这个节点是共享模式的
        boolean failed = true;
        try {
            for (;;) {
                final Node p = node.predecessor();  //当前节点的前驱节点
                if (p == head) {                   //如果当前节点的前驱节点是头节点(至于为什么是前驱节点而不是当前节点,原因参考enq方法)
                    int r = tryAcquireShared(arg);  //再次尝试获取信号量
                    if (r >= 0) {                   //如果成功,则将当前的节点设置成头节点,并释放信号量  
                        setHeadAndPropagate(node, r);
                        p.next = null; // help GC
                        failed = false;
                        return;
                    }
                }
                if (shouldParkAfterFailedAcquire(p, node) &&            //检查是否应该挂起失败的线程。这里通常返回的false,不会抛出异常
                    parkAndCheckInterrupt())
                    throw new InterruptedException();
            }
        } finally {
            if (failed)
                cancelAcquire(node);            //注销当前节点
        }
    }
/**
     * Creates and enqueues node for current thread and given mode.
     *
     * @param mode Node.EXCLUSIVE for exclusive, Node.SHARED for shared
     * @return the new node
     */
    private Node addWaiter(Node mode) {
//将当前线程封装进node Node node
= new Node(Thread.currentThread(), mode); // Try the fast path of enq; backup to full enq on failure
//把节点添加进资源队列末尾tail Node pred = tail; if (pred != null) { //如果末尾节点不为空,则采用CAS操作将当前节点添加进队列末尾 node.prev = pred; //当前节点的前驱节点,设置为末尾节点 if (compareAndSetTail(pred, node)) { pred.next = node; return node; } } enq(node); //初始化队列 return node; }
 /**
     * Inserts node into queue, initializing if necessary. See picture above.
     * @param node the node to insert
     * @return node's predecessor
     */
    private Node enq(final Node node) {
        for (;;) {          //采取死循环,添加节点
            Node t = tail;
            if (t == null) { // Must initialize
                if (compareAndSetHead(new Node()))             //如果末尾节点为空,则证明队列为空,新添加一个节点,作为头结点和末尾节点
                    tail = head;
            } else {       //已经有末尾节点,则采取CAS操作把当前节点添加进队列末尾,成为末尾节点
                node.prev = t;
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
    }
//注意这里的代码实际执行效果。分为两步:1、先检查末尾节点是否为空,如果空,证明队列为空,则添加一个新节点,同时作为头节点和末尾界定啊
//2、如果末尾节点不为空(队列已经初始化),则采用CAS操作把当前节点添加队列末尾
//其中,步骤2是肯定会执行的
/**
     * Sets head of queue, and checks if successor may be waiting
     * in shared mode, if so propagating if either propagate > 0 or
     * PROPAGATE status was set.
     *
     * @param node the node
     * @param propagate the return value from a tryAcquireShared
     */
    private void setHeadAndPropagate(Node node, int propagate) {
        Node h = head; // Record old head for check below
        setHead(node);
        /*
         * Try to signal next queued node if:
         *   Propagation was indicated by caller,
         *     or was recorded (as h.waitStatus either before
         *     or after setHead) by a previous operation
         *     (note: this uses sign-check of waitStatus because
         *      PROPAGATE status may transition to SIGNAL.)
         * and
         *   The next node is waiting in shared mode,
         *     or we don't know, because it appears null
         *
         * The conservatism in both of these checks may cause
         * unnecessary wake-ups, but only when there are multiple
         * racing acquires/releases, so most need signals now or soon
         * anyway.
         */

//如果当前节点没有了下一个节点或者下一个节点已经是共享模式,则可释放当前节点。 if (propagate > 0 || h == null || h.waitStatus < 0 || (h = head) == null || h.waitStatus < 0) { Node s = node.next; if (s == null || s.isShared()) doReleaseShared(); } }
/**
     * Release action for shared mode -- signals successor and ensures
     * propagation. (Note: For exclusive mode, release just amounts
     * to calling unparkSuccessor of head if it needs signal.)
     */
    private void doReleaseShared() {
        /*
         * Ensure that a release propagates, even if there are other
         * in-progress acquires/releases.  This proceeds in the usual
         * way of trying to unparkSuccessor of head if it needs
         * signal. But if it does not, status is set to PROPAGATE to
         * ensure that upon release, propagation continues.
         * Additionally, we must loop in case a new node is added
         * while we are doing this. Also, unlike other uses of
         * unparkSuccessor, we need to know if CAS to reset status
         * fails, if so rechecking.
         */
        for (;;) {
            Node h = head;
            if (h != null && h != tail) {
                int ws = h.waitStatus;
                if (ws == Node.SIGNAL) {             //如果status已经是SIGNAL了,将当前节点的status使用CAS设置成0。注意这里,有continue
                    if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                        continue;            // loop to recheck cases
                    unparkSuccessor(h);      //不挂起后继节点
                }
                else if (ws == 0 &&          //status设置成0或者本身已经是0,则采取CAS操作,将status设置成PROPAGATE,也就是-3状态
                         !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                    continue;                // loop on failed CAS
            }
            if (h == head)                   // loop if head changed   //如果头节点没有变化,证明头节点没有被其他节点修改,则释放成功
                break;
        }
    }

//从这里看出,所谓的释放信号,就是将头结点(刚才已经把获取到信号量的节点设置成头节点)的状态设置成PROPAGATE -3的状态
 /**
     * Checks and updates status for a node that failed to acquire.
     * Returns true if thread should block. This is the main signal
     * control in all acquire loops.  Requires that pred == node.prev.
     *
     * @param pred node's predecessor holding status
     * @param node the node
     * @return {@code true} if thread should block
     */
    private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus;
        if (ws == Node.SIGNAL)
            /*
             * This node has already set status asking a release
             * to signal it, so it can safely park.
             */
            return true;
        if (ws > 0) {
            /*
             * Predecessor was cancelled. Skip over predecessors and
             * indicate retry.
             */
            do {
                node.prev = pred = pred.prev;       //死循环直到找到状态<=0的前驱节点
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
            /*
             * waitStatus must be 0 or PROPAGATE.  Indicate that we
             * need a signal, but don't park yet.  Caller will need to
             * retry to make sure it cannot acquire before parking.
             */
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }
//如果前驱节点已经是SIGNAL -1 唤醒状态,直接返回true。
//但是对于信号量的实际执行,前驱节点不是SIGNAL,则执行到else代码块
//所以,这个方法的实际作用,就是讲前驱节点的状态设置成SIGNAL,并返回false

总结:获取信号量的时候,如果信号量还有,则获取成功。否则,将获取失败的线程,添加进队列里面(包含队列初始化)。

然后,采用死循环执行阻塞。当一个线程释放了信号量后,队列的第二个节点(注意这里不是头节点)会采用死循环获取信号量,成功后则将自设置成头节点,

同时设置自己的状态为PROPAGATE -3。这其中还包括线程中断后,挂起线程。挂起后驱节点、注销节点等其他辅助性操作。

 

 ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

 下面看下释放信号量release() 

public void release() {
        sync.releaseShared(1);   //这个方法,调用父类AQS的releaseShared()方法
}
//释放共享信号量
public
final boolean releaseShared(int arg) { if (tryReleaseShared(arg)) { //尝试释放信号量,这里调用的是Semaphore的方法 doReleaseShared(); //这里真正释放信号量,参考上面的代码说明 return true; } return false; }
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;
            }
        }

从这里可以看出,所谓的尝试释放信号量,就是采用CAS操作,将AQS的state变量叠减。

而真正释放信号量,则是将头节点的状态改成PROPAGATE -3

 -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

其他方法

获取可用信号量数量,availablePermits(),就是获取AQS的state变量的值。

重置信号量,drainPermits(),采用CAS操作将state重置为0

叠减信号量,reducePermits(int reduction),采用CAS操作叠减 

是否形成队列,boolean hasQueuedThreads(),判断头节点是否不等于末尾节点

另外:

根据源代码说法,如果同时有两个线程a和b,a线程执行release(),b线程执行acquire(),那么根据happen-before规则,a线程将会在b线程之前执行。

Memory consistency effects: Actions in a thread prior to calling
* a "release" method such as {@code release()}
* <a href="package-summary.html#MemoryVisibility"><i>happen-before</i></a>
* actions following a successful "acquire" method such as {@code acquire()}
* in another thread

posted on 2021-02-10 12:07  drafire  阅读(138)  评论(0编辑  收藏  举报