Java并发之Semaphore源码解析(二)

在上一章,我们学习了信号量(Semaphore)是如何请求许可证的,下面我们来看看要如何归还许可证。

可以看到当我们要归还许可证时,不论是调用release()或是release(int permits),都会调用AQS实现的releaseShared(int arg)方法。在releaseShared(int arg)方法中会先调用子类实现的tryReleaseShared(int arg)方法,这个方法会向信号量归还许可证,在归还完毕后,会调用doReleaseShared()方法尝试唤醒信号量等待队列中需要许可证的线程,这也印证了笔者之前所说的线程在归还信号量后,会尝试唤醒等待队列中等待许可证的线程。

那我们来看看信号量(Semaphore)静态内部类Sync实现的tryReleaseShared(int releases)是怎么完成归还许可证,首先会调用getState()获取信号量当前剩余的许可证,加上外部线程归还的许可证数量算出总许可证数量:current + releases,如果能用CAS的方式修改成功,则退出方法,否则一直轮询直到归还成功,这里CAS失败的原因有可能是外部也在请求和归还许可证,可能在执行完代码<1>处后和执行代码<2>处之前,信号量内部的许可证数量已经变了,所以CAS失败。归还信号量成功后就会调用doReleaseShared(),这个方法前面已经讲解过了,这里就不再赘述了。

public class Semaphore implements java.io.Serializable {

    //...

    abstract static class Sync extends AbstractQueuedSynchronizer {

        //...

        protected final boolean tryReleaseShared(int releases) {

            for (;;) {

                int current = getState();//<1>

                int next = current + releases;

                if (next < current) // overflow

                    throw new Error("Maximum permit count exceeded");

                if (compareAndSetState(current, next))//<2>

                    return true;

            }

        }

        //...

    }

    //...

    public void release() {

        sync.releaseShared(1);

    }

    //...

    public void release(int permits) {

        if (permits < 0) throw new IllegalArgumentException();

        sync.releaseShared(permits);

    }

    //...

}

public abstract class AbstractQueuedSynchronizer

    extends AbstractOwnableSynchronizer

    implements java.io.Serializable {

    //...

    public final boolean releaseShared(int arg) {

        if (tryReleaseShared(arg)) {

            doReleaseShared();

            return true;

        }

        return false;

    }

    //...

    protected boolean tryReleaseShared(int arg) {

        throw new UnsupportedOperationException();

    }

    //...

}

下面我们再来看看tryAcquire(long timeout, TimeUnit unit)和tryAcquire(int permits, long timeout, TimeUnit unit)的实现,这两个方法会在给定的时间范围内尝试获取许可证,如果获取成功则返回true,获取失败则返回false。

这两个方法都会调用AQS实现的tryAcquireSharedNanos(int arg, long nanosTimeout),这个方法其实和先前讲得doAcquireShared(int arg)十分相似,只是多了一个超时返回的功能。

这里笔者简单过一下这个方法的实现:先在代码<1>处算出超时时间,然后封装线程对应的节点Node并将其入队,如果判断节点的前驱节点是头节点,且申请许可证成功,这里会调用setHeadAndPropagate(node, r)将头节点指向当前节点,并尝试唤醒下一个节点对应的线程。如果申请许可证失败,会在<2>处算出还剩多少的阻塞时间nanosTimeout,如果剩余阻塞时间小于等于0,代表线程获取许可证失败,这里会调用<3>处的cancelAcquire(node) 将节点从等待队列中移除,具体的移除逻辑可以看笔者写的ReentrantLock源码解析第二章。如果剩余阻塞时间大于0,则会执行shouldParkAfterFailedAcquire(p, node)将前驱节点的等待状态改为SIGNAL,在第二次循环时,如果前驱节点的状态为SIGNAL,且剩余阻塞时间大于SPIN_FOR_TIMEOUT_THRESHOLD(1000ns),则陷入阻塞,直到被中断抛出异常,或者被唤醒,检查是否能获取许可证,如果不能获取许可证且超时,则会返回false表示在超时时间内没有获取到许可证。

public class Semaphore implements java.io.Serializable {

    //...

    public boolean tryAcquire(int permits, long timeout, TimeUnit unit)

        throws InterruptedException {

        if (permits < 0) throw new IllegalArgumentException();

        return sync.tryAcquireSharedNanos(permits, unit.toNanos(timeout));

    }

    //...

    public boolean tryAcquire(long timeout, TimeUnit unit)

        throws InterruptedException {

        return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));

    }

    //...

}

public abstract class AbstractQueuedSynchronizer

    extends AbstractOwnableSynchronizer

    implements java.io.Serializable {

    //...

    public final boolean tryAcquireSharedNanos(int arg, long nanosTimeout)

            throws InterruptedException {

        if (Thread.interrupted())

            throw new InterruptedException();

        return tryAcquireShared(arg) >= 0 ||

            doAcquireSharedNanos(arg, nanosTimeout);

    }

    //...

    private boolean doAcquireSharedNanos(int arg, long nanosTimeout)

            throws InterruptedException {

        if (nanosTimeout <= 0L)

            return false;

        final long deadline = System.nanoTime() + nanosTimeout;//<1>

        final Node node = addWaiter(Node.SHARED);

        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

                        return true;

                    }

                }

                nanosTimeout = deadline - System.nanoTime();//<2>

                if (nanosTimeout <= 0L) {

                    cancelAcquire(node);//<3>

                    return false;

                }

                if (shouldParkAfterFailedAcquire(p, node) &&

                    nanosTimeout > SPIN_FOR_TIMEOUT_THRESHOLD)

                    LockSupport.parkNanos(this, nanosTimeout);

                if (Thread.interrupted())

                    throw new InterruptedException();

            }

        } catch (Throwable t) {

            cancelAcquire(node);

            throw t;

        }

    }

    //...

}

下面我们对照一下FairSync和NonfairSync,其实NonfairSync基本没有什么实现,都是调用其父类Sync的方法,以非公平的方式竞争许可证也是调用其父类nonfairTryAcquireShared(acquires)方法。而FairSync自身是有实现以公平的方式获取许可证,实现逻辑也非常简单。先判断信号量的等待队列是否有节点,有的话则返回获取失败,如果没有再获取当前的可用许可证数量available,扣去申请的许可证数量available - acquires,用CAS的方式把扣减完的值remaining存放进state,由于扣减的时候可能存在其他线程也在申请/归还许可证,所以available的值并非一直有效,如果在获取available后有其他线程也申请和归还许可证,那么这里的CAS很可能会失败,判断CAS失败后,又会开始新的一轮尝试获取许可证逻辑。

static final class FairSync extends Sync {

    private static final long serialVersionUID = 2014338818796000944L;

    FairSync(int permits) {

        super(permits);

    }

    protected int tryAcquireShared(int acquires) {

        for (;;) {

            if (hasQueuedPredecessors())

                return -1;

            int available = getState();

            int remaining = available - acquires;

            if (remaining < 0 ||

                compareAndSetState(available, remaining))

                return remaining;

        }

    }

}

static final class NonfairSync extends Sync {

    private static final long serialVersionUID = -2694183684443567898L;

    NonfairSync(int permits) {

        super(permits);

    }

    protected int tryAcquireShared(int acquires) {

        return nonfairTryAcquireShared(acquires);

    }

}

对照完公平FairSync和非公平NonfairSync的差别后,我们来看看Sync类实现的方法,Sync类的实现其实也不算复杂,主要就下面4个方法,其中:nonfairTryAcquireShared(int acquires)和tryReleaseShared(int releases)先前已经将结果了,下面我们专注:reducePermits(int reductions)和drainPermits()。


abstract static class Sync extends AbstractQueuedSynchronizer {

    final int nonfairTryAcquireShared(int acquires) {

        //...

    }

    protected final boolean tryReleaseShared(int releases) {

        //...

    }

    final void reducePermits(int reductions) {

        //...

    }

    final int drainPermits() {

        //...

    }

}

Sync类实现的的reducePermits(int reductions)的作用是降低许可证数量,比如当双11来临时,淘宝京东可以对一些服务进行扩容和配置升级,使得原本可以承受10W并发量的服务提高到可以承受50W,这里可以在不调用acquire()的前提下,调用release()方法增加信号量的许可证,当双11的压力过去后,需要对服务进行缩容,由50W的并发量回到10W,这里可以用reducePermits(int reductions)降低许可证数量。在这个方法中会先获取当前许可证数量,减去我们要扣除的许可证数量current - reductions,并判断其结果是否溢出,如果溢出则抛出异常,没有溢出用CAS的方式设置最新的许可证数量。


10

11

12

13

14

15

16

17

18

19

20

21

22

23

public class Semaphore implements java.io.Serializable {

    //...

    abstract static class Sync extends AbstractQueuedSynchronizer {

        //...

        final void reducePermits(int reductions) {

            for (;;) {

                int current = getState();

                int next = current - reductions;

                if (next > current) // underflow

                    throw new Error("Permit count underflow");

                if (compareAndSetState(current, next))

                    return;

            }

        }

        //...

    }

    //...

    protected void reducePermits(int reduction) {

        if (reduction < 0) throw new IllegalArgumentException();

        sync.reducePermits(reduction);

    }

    //...

}

需要注意两点:

这个方法的访问权限是protected,如果要使用此方法需要用一个类去继承,并修改此方法的访问权限。

这个方法可能导致信号量的剩余许可证数量为负,比如一个信号量原先的许可证数量为10,且被借走了9个许可证,当前许可证数量为1。这时想把许可证数量从原先的10扣降到3,向reducePermits(int reduction)传入7,此时current-reductions=1-7=-6,如果CAS成功,那么信号量目前的许可证数量为-6,不过没关系,如果前面借走的9个许可证最终会归还,信号量的许可证数量最终会回到3。

class MySemaphore extends Semaphore {

    public MySemaphore(int permits) {

        super(permits);

    }

    @Override

    public void reducePermits(int reduction) {

        super.reducePermits(reduction);

    }

}

public static void main(String[] args) {

    MySemaphore semaphore = new MySemaphore(8);

    System.out.println("初始信号量的许可证数量:" + semaphore.availablePermits());

    //初始化完信号量后,增加信号量的许可证数量

    int add = 2;

    semaphore.release(add);

    System.out.printf("增加%d个许可证后,许可证数量:%d\n", add, semaphore.availablePermits());

    //申请9个许可证

    int permits = 9;

    try {

        semaphore.acquire(permits);

        System.out.printf("申请%d个许可证后剩余许可证数量:%d\n", permits, semaphore.availablePermits());

    } catch (InterruptedException e) {

        e.printStackTrace();

    }

    //这里要将原先10个许可证扣除到只剩3个,所以传入7,扣除7个许可证

    semaphore.reducePermits(7);

    System.out.println("扣除7个许可证数量后,剩余许可证数量:" + semaphore.availablePermits());

    //归还原先出借的9个许可证

    semaphore.release(permits);

    System.out.printf("归还原先出借的%d信号量后,剩余信号量:%d\n", permits, semaphore.availablePermits());

}

执行结果:

1

2

3

4

5

初始信号量的许可证数量:8

增加2个许可证后,许可证数量:10

申请9个许可证后剩余许可证数量:1

扣除7个许可证数量后,剩余许可证数量:-6

归还原先出借的9信号量后,剩余信号量:3

Sync类实现的drainPermits()可以一次性扣除信号量目前所有的许可证数量并返回,通过这个API,我们可以得知资源目前最大的访问限度。还是拿上一章远程服务为例,判定服务能承受的并发是5000,用于限流的semaphore信号量的最大许可证数量也是5000。假设目前信号量剩余的许可证数量为2000,即有3000个线程正在并发访问远程服务,我们可以通过drainPermits()方法获取剩余的允许访问数量2000,然后创建2000个线程访问远程服务,这个API一般用于计算量大且计算内容比较独立的场景。


public class Semaphore implements java.io.Serializable {

    //...

    abstract static class Sync extends AbstractQueuedSynchronizer {

        //...

        final int drainPermits() {

            for (;;) {

                int current = getState();

                if (current == 0 || compareAndSetState(current, 0))

                    return current;

            }

        }

        //...

    }

    //...

    public int drainPermits() {

        return sync.drainPermits();

    }

    //...

}

最后,笔者介绍一个Semaphore在JDK1.6.0_17时期的BUG,便结束对Semaphore的源码解析。

当时AQS的setHeadAndPropagate(Node node, int propagate)和releaseShared(int arg) 两个方法的实现是下面这样的,这个代码可能导致队列被阻塞。


private void setHeadAndPropagate(Node node, int propagate) {

    setHead(node);

    if (propagate > 0 && node.waitStatus != 0) {

        Node s = node.next;

        if (s == null || s.isShared())

            unparkSuccessor(node);

    }

}

public final boolean releaseShared(int arg) {

    if (tryReleaseShared(arg)) {

        Node h = head;

        if (h != null && h.waitStatus != 0)

            unparkSuccessor(h);

        return true;

    }

    return false;

}

按照上面代码的实现,会让下面的代码出现队列被阻塞的情况。t1和t2线程用于请求许可证,t3和t4线程用于归还许可证,循环10000000次只是为了增加出现阻塞的概率,现在说说什么样的场景下会出现队列被阻塞的情况。

程序开始时,信号量的许可证数量为0,所以t1和t2只能进入队列等待,t1和t2在队列中的节点对应N1和N2,节点的排序为:head->N1->N2(tail)。t3归还许可证时发现头节点不为null且头节点的等待状态为SIGNAL,于是会调用unparkSuccessor(h)方法唤醒头节点的后继节点N1对应的线程t1,在执行unparkSuccessor(h)的时候会把head的等待状态改为0。

t1被唤醒后获取到许可证,返回剩余许可证数量为0,即之后调用setHeadAndPropagate(Node node, int propagate)方法传入的propagate为0,但尚未调用。此时t4也归还了许可证,但发现head节点的等待状态为0,就不会调用unparkSuccessor(h)。

t1执行setHeadAndPropagate(Node node, int propagate),将头节点指向自身线程对应的节点N1,虽然此时信号量里有剩余的许可证,但t1原先拿到的propagate为0,所以不会执行unparkSuccessor(node)唤醒t4。

那么新版本的setHeadAndPropagate(Node node, int propagate)和releaseShared(int arg)又是如何保证有许可证被归还时唤醒队列中被阻塞的线程呢?这里其实和PROPAGATE有关,让我们按照新版的setHeadAndPropagate和releaseShared走一遍上面的流程。

t1和t2进入队列中等待,t3归还许可证发现头节点不为null,且头节点等待状态为SIGNAL,于是调用unparkSuccessor(h)方法唤醒头节点的后继节点N1对应的线程t1,在执行unparkSuccessor(h)的时候会把head的等待状态改为0。

t1被唤醒后获取到许可证,返回剩余许可证数量为0,在调用setHeadAndPropagate(Node node, int propagate)之前,t4归还了许可证,发现头节点的等待状态为0,将其改为PROPAGATE。

t1执行setHeadAndPropagate(Node node, int propagate),获取原先头节点h,并将头节点指向N1,此时虽然propagate为0,但原先头节点h的等待状态<0,可以执行doReleaseShared()唤醒后继节点N2对应的线程t2。


import java.util.concurrent.Semaphore;

public class TestSemaphore {

    private static Semaphore sem = new Semaphore(0);

    private static class Thread1 extends Thread {

        @Override

        public void run() {

            sem.acquireUninterruptibly();

        }

    }

    private static class Thread2 extends Thread {

        @Override

        public void run() {

            sem.release();

        }

    }

    public static void main(String[] args) throws InterruptedException {

        for (int i = 0; i < 10000000; i++) {

            Thread t1 = new Thread1();

            Thread t2 = new Thread1();

            Thread t3 = new Thread2();

            Thread t4 = new Thread2();

            t1.start();

            t2.start();

            t3.start();

            t4.start();

            t1.join();

            t2.join();

            t3.join();

            t4.join();

            System.out.println(i);

        }

    }

}

USB Microphone https://www.soft-voice.com/

Wooden Speakers  https://www.zeshuiplatform.com/

亚马逊测评 www.yisuping.cn

深圳网站建设www.sz886.com

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 156,907评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,546评论 1 289
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,705评论 0 238
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,624评论 0 203
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,940评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,371评论 1 210
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,672评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,396评论 0 195
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,069评论 1 238
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,350评论 2 242
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,876评论 1 256
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,243评论 2 251
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,847评论 3 231
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,004评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,755评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,378评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,266评论 2 259

推荐阅读更多精彩内容

  • 常见问题:对某个知识点的理解或看法,一般从是什么,原理,好处与应用场景来回答你对AQS的理解(想法)?CountD...
    _code_x阅读 1,373评论 1 14
  • 前言 Semaphore(信号量)是用来控制同时访问特定资源的线程数量,它通过协调各个线程,以保证合理的使用公共资...
    nicktming阅读 282评论 0 2
  • 我是黑夜里大雨纷飞的人啊 1 “又到一年六月,有人笑有人哭,有人欢乐有人忧愁,有人惊喜有人失落,有的觉得收获满满有...
    陌忘宇阅读 8,470评论 28 53
  • 信任包括信任自己和信任他人 很多时候,很多事情,失败、遗憾、错过,源于不自信,不信任他人 觉得自己做不成,别人做不...
    吴氵晃阅读 6,132评论 4 8
  • 步骤:发微博01-导航栏内容 -> 发微博02-自定义TextView -> 发微博03-完善TextView和...
    dibadalu阅读 3,094评论 1 3