zoukankan      html  css  js  c++  java
  • 信号量,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
  • 相关阅读:
    PHP数组的升序降序函数
    overflow样式修改插件——jscrollpane
    li标签之间的空隙问题(转)
    纯CSS制作三角(转)
    图片放大插件——elevatezoom
    git常用命令笔记
    求数组子序列和最大值
    Vim快速入门
    C++求两个整数的最大公约数和最小公倍数
    实验楼-4-Linux文件权限
  • 原文地址:https://www.cnblogs.com/drafire/p/14393591.html
Copyright © 2011-2022 走看看