JUC - Semaphore应用与源码解析

Semaphore,信号量通常用于限流。

简单使用

    public static void main(String[] args){
        Semaphore s = new Semaphore(3, true);
        for(int i = 1; i <= 10; i++){
            new Thread(() -> {
                try {
                    s.acquire();
                    System.out.println(Thread.currentThread().getName());
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    s.release();
                }
            }, "t"+i).start();
        }
    }

工作原理概述

构造函数中指定信号数量permits,最终该值被赋给了AQS中的state变量,state的取值代表当前可以获取的信号量总数。acquire(n)尝试state-n,release(n)尝试state+n。

源码讲解

接下来详细讲解几个主要函数

构造函数

    public Semaphore(int permits, boolean fair) {
    	//true:公平;false:非公平
        sync = fair ? new FairSync(permits) : new NonfairSync(permits);
    }

无论公平还是非公平,最终都会调用AQS的setState,设置state值为permits。这里很简单,自己看一看源码就好了。

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

acquire(n)

    public final void acquireSharedInterruptibly(int arg)
            throws InterruptedException {
        //首先检查当前线程的中断状态位,如果被中断了,就抛出InterruptedException。
        //注意,interrupted()会重置中断状态位,即抛出异常之后,该线程中断状态会被抹去。
        if (Thread.interrupted())
            throw new InterruptedException();
        //至少会执行一次tryAcquireShared,这是一个模板方法,供子类重写。若返回正数说明获取信号量成功,函数结束。
        //如果是非公平模式,最终会调用nonfairTryAcquireShared方法,再下文讲解;
        //如果是公平模式,最终会调用FairSync的tryAcquireShared方法,下文详解。
        if (tryAcquireShared(arg) < 0)
        	//如果尝试获取信号量失败,就会入队
            doAcquireSharedInterruptibly(arg);
    }

我们首先看一下非公平模式下,nonfairTryAcquireShared的实现,这个比较容易理解。

        final int nonfairTryAcquireShared(int acquires) {
        	//一个死循环
            for (;;) {
            	//获得state的当前值。记得上文说过,state代表了目前可用的信号量总数。
                int available = getState();
                int remaining = available - acquires;
                //remaining代表减去acquires之后,还剩下信号量的数目。
                //如果剩下数目小于0,说明当前的信号量不够,此时return remainging(负数),意味着尝试获取失败
                //如果remaining>=0,CAS设置state值为remaining,若CAS成功,return 正数,代表尝试获取信号量成功
                //可以看到,若信号量数目够用,且一直CAS失败,就会一直循环下去
                if (remaining < 0 ||
                    compareAndSetState(available, remaining))
                    return remaining;
            }
        }

在看一下公平模式下tryAcquireShared的实现。可以看到,相比较于非公平模式,公平模式首先会检查当前线程是否需要排队。若需要排队代表此次尝试获取信号量失败,要进队列排队。若不需要排队,后面的逻辑和上文的nonfairTryAcquireShared是一样的。

        protected int tryAcquireShared(int acquires) {
            for (;;) {
            	//hasQueuedPredecessors,检查当前线程是否需要排队。
            	//这个方法短小精悍,却十分重要,在JUC多出都有应用,下面会详细分析。
            	//若返回true,则直接返回-1,此次尝试获取信号量失败
                if (hasQueuedPredecessors())
                    return -1;
                int available = getState();
                int remaining = available - acquires;
                if (remaining < 0 ||
                    compareAndSetState(available, remaining))
                    return remaining;
            }
        }

下面这个方法是用来判断当前线程是否需要入队列排队。在详细剖析之前,首先申明一点:
队列头节点是不参与排队的,队列中的第二个节点是第一个参与排队的节点。
类比买火车票,第一个人正在买票,他能算是排队吗?显然不能。他后面的那个人才是第一个排队的人。
因此,你记住,在AQS里,头结点永远不参与排队。这点很重要。

    public final boolean hasQueuedPredecessors() {
    	//队列尾节点
        Node t = tail;
        //队列头结点
        Node h = head;
        Node s;
        //返回值分情况讨论:
        //1. head和tail都是null。此时h!=t 是false。方法直接返回false。代表不需要排队。
        //这说明此前尝试获取信号量时,信号量一直够用,且每次CAS扣减信号量都成功。参考tryAcquireShared
        //head和tail不可能一个是null,一个不是null,那么接下来就讨论两者都不是null的情况。
        //2. head和tail都不是null,且h==t。也就是队列中只有一个节点,它既是head也是tail。
        //只有一个节点是什么情况呢?第一个线程来排队的时候,会新建一个虚拟节点作为head,
        //自己所属的Node节点跟在后面,所以会有两个节点。只有一个节点是这种情况:
        //某个时刻队列中有n个节点在排队,后来陆续地n个节点都获得了所需信号量,
        //此时队列中就只剩下一个head节点。head节点是不参与排队的,也就是目前队列里没有排队的线程。
        //那么当前线程也不排队,先去尝试扣减state。此时,h==t,方法返回false。
        //3. head和tail都不是null,且h!=t。此时h!=t 是true。(s = h.next) == null是false。
        //接下来看这个条件:s.thread != Thread.currentThread()。s.thread代表第一个排队的线程
        //如果当前线程不是第一个排队的线程,那么没啥说的,在你之前有线程排队,你就乖乖到队列后面排队吧。返回true
        //如果当前线程是第一个排队的线程,那么 s.thread != Thread.currentThread()是false。方法整体返回false
        //这种情况不太明白,当前线程如果正在排队,它应该是被park了才对,怎么会又一次来排队呢?
        return h != t &&
            ((s = h.next) == null || s.thread != Thread.currentThread());
    }

以上,分析了尝试获取信号量,若尝试失败,则执行下面的方法。看字面含义是:
以共享模式可中断地获取信号量

    private void doAcquireSharedInterruptibly(int arg)
        throws InterruptedException {
        //addWaiter方法向队列中新增一个共享模式的Node节点,源码在下面分析
        final Node node = addWaiter(Node.SHARED);
        boolean failed = true;
        try {
            for (;;) {
            	//拿到node的前置节点p
                final Node p = node.predecessor();
                //如果前直接点是head,也就是说自己是第一个排队的线程。
                if (p == head) {
                	//再次尝试获取信号量。有人会说,前面尝试过一次,失败了才来入队的,在尝试一次是不是浪费?
                	//非也。上一次尝试失败了,现在进入队列去排队了,发现自己竟然是第一个排队的,再去尝试一次很合理。
                    int r = tryAcquireShared(arg);
                    //r >=0说明尝试获取信号量成功。当然,几率很小。
                    //如果成功了,就把自己设置为头结点head。这也印证了上文的分析。队列中只有一个节点的情况,
                    //就是对应于原来所有排队的线程都成功获得了信号量。而这唯一的节点就是原先的最后一个排队的节点。
                    if (r >= 0) {
                    	//
                        setHeadAndPropagate(node, r);
                        p.next = null; // help GC
                        failed = false;
                        return;
                    }
                }
                //如果不是第一个排队的或者尝试获取信号量失败,就走到了这里,既然要排队,当前线程就要park(沉睡)了。
                //但是自己什么时候能醒来呢?所以在沉睡前,先把前置节点的waitStatus置为SIGNAL(-1),
                //意为,当前置节点成功获取到信号量,退出队列的时候叫醒自己。
                //shouldParkAfterFailedAcquire就是把前置节点状态设置为SIGNAL
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    throw new InterruptedException();
            }
        } finally {
        	//如果执行到这里。说明当前线程在排队过程中被中断了。那么终止当前获取行为。
                cancelAcquire(node);
        }
    }

addWaiter方法,增加新的节点至排队队列中。

    private Node addWaiter(Node mode) {
        Node node = new Node(Thread.currentThread(), mode);
        Node pred = tail;
        //此处检查tail是不是null,其实就是检查队列有没有初始化。这两者是等价的。
        //前面分析过,只要初始化过,队列至少会有一个节点,tail不会是null。
        //若tail不是null,那么CAS把当前节点追加至tail后面,成为新的tail。
        if (pred != null) {
            node.prev = pred;
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
        //若tail==null,或者CAS操作失败,则执行enq,下面解析
        enq(node);
        return node;
    }
    private Node enq(final Node node) {
    	//死循环
        for (;;) {
            Node t = tail;
            //如果tail是null,说明未初始化队列,则新建一个节点,head和tail都 指向它。
            if (t == null) { // Must initialize
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {
            	//若tail不为空,则CAS设置node为新的tail,直至成功。因为这在一个死循环里。
                node.prev = t;
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
    }

至此,Semaphore的acquire(n)方法解析完毕。

release(n)

    public final boolean releaseShared(int arg) {
    	//CAS尝试修改state变量的值
        if (tryReleaseShared(arg)) {
        	//释放队列中的相应节点
            doReleaseShared();
            return true;
        }
        return false;
    }
    private void doReleaseShared() {
        for (;;) {
            Node h = head;
            if (h != null && h != tail) {
                int ws = h.waitStatus;
                //如果当前节点的ws是SIGNAL,代表它有义务叫醒后续节点
                if (ws == Node.SIGNAL) {
                    if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                        continue;            // loop to recheck cases
                    //叫醒后续节点,下文做详细分析
                    unparkSuccessor(h);
                }
                //目前没有领悟到设置ws为PROPAGATE有什么用
                else if (ws == 0 &&
                         !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                    continue;                // loop on failed CAS
            }
            if (h == head)                   // loop if head changed
                break;
        }
    }
    private void unparkSuccessor(Node node) {
        /*
         * If status is negative (i.e., possibly needing signal) try
         * to clear in anticipation of signalling.  It is OK if this
         * fails or if status is changed by waiting thread.
         */
        int ws = node.waitStatus;
        if (ws < 0)
            compareAndSetWaitStatus(node, ws, 0);

        /*
         * Thread to unpark is held in successor, which is normally
         * just the next node.  But if cancelled or apparently null,
         * traverse backwards from tail to find the actual
         * non-cancelled successor.
         */
         //英文注释说的很明白了。我有一点不明,为什么要从尾节点倒叙遍历?
        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);
    }

你可能感兴趣的:(java并发)