关于java:我靠Semaphore里面居然有这么一个大坑

31次阅读

共计 7634 个字符,预计需要花费 20 分钟才能阅读完成。

这是 why 的第 59 篇原创文章

荒腔走板

大家好,我是 why 哥,欢送来到我间断周更优质原创文章的第 59 篇。

上周写了一篇文章,一不小心戳到了大家的爽点,其中一个转载我文章的大号,浏览量竟然冲破了 10w+,我也是受宠若惊。

然而其实我是一个技术博主来的,偶然写点生存相干的。所以这篇还是回到技术上。

然而我的技术文章有个特点是第一张图片都是我本人拍的。而后我会围绕这个图片进行一个简短的形容,我称之为荒腔走板环节。

目标是给寒冷的技术文注入一丝色调。

我这样做曾经保持了很多篇,有的读者给我说:看完荒腔走板局部就退出去了。

那你们是真的棒哦,至多退出去之前,拉到文末,来个一键三连吧,给我来点正反馈。

好了,先说说这期的荒腔走板。

下面这个图片是我上周末看《乐队的夏天》的时候拍的。

这个乐队的名字叫做水木年华,我喜爱这个乐队。

我听他们的歌的时候,应该是初中,那个时候磁带曾经差不多快过气了,进入了光碟的时代,我记得一张光碟外面有好几十首歌,第一次在 DVD 外面听到他们的歌是《毕生有你》,听到这首歌的时候就感觉很洁净,很惊艳。

而后一字一句抄在本人的歌词本上。

听到这首歌的那个周末,我就看着那个 MV 重复学,那时的 DVD 有个性能是能够 A-B 重复播放某个片段,我就一句一句的学,学会了这首歌。

那时候的李健,一双明澈亮堂的大眼睛,就像一汪湖水,我一个小男孩,都好想在他的眼睛里扎个猛子。

这首歌,我愿称之为校园民谣的巅峰之一。

十多年后的明天,这个乐队从新呈现在我的视线中,只是李健曾经不再其中。

他们在乐夏的舞台上唱了一首《青春再见》,后果被一个自称 23 岁的胖小伙说“中年人的清淡”,被另个业余乐迷说:“四十多岁的人怎么还在唱青春再见?”。第一期就被淘汰出局。

这操作,看的我一愣一愣的。

这个怎么就清淡了?四十多岁的人怎么就不能唱青春再见了?男人至死都是少年你们不晓得吗?小子,他们玩音乐的时候你还不会谈话呢。

他们来到舞台的画面,我感觉到一丝辛酸,一丝真的青春再见的辛酸。

水木年华没有错,错的是这个舞台,这个舞台不适宜他们的歌曲。

好了,说回文章。

一起看个问题

前几天有个读者给我发了一个链接,说这个链接外面的代码,为什么会这样运行,切实是没有搞懂是怎么回事,链接如下:

https://springboot.io/t/topic/1139

代码是这样的,给大家上个图:

留神第 10 行,permits 参数,依据他的形容应该是 3:

不晓得为什么代码外面给了一个 2。然而为了保障实在,我间接拿过去了,没有进行改变。一会我会依据这个代码进行简略的批改。

晓得 semaphore 是干啥的同学能够先看看下面的代码,为什么造成了“死锁”。

反正是一个十分无语的低级谬误,然而我重复看了几遍竟然没有看进去。

不晓得 semaphore 是干啥的同学,看过去。我先给你科普一下。

semaphore 咱们个别叫它信号量,用来 管制同时拜访指定资源的线程数量

如果不懂 semaphore,那下面代码你也看不懂了,我依照代码的逻辑给你举个例子。

比方一个高端停车场,只有 3 个车位。(这就是“指定资源”)

当初外面没有停车,那么它最多能够停几辆车呢?

是的,门口的残余车辆指示牌显示:残余停车位 3 辆。

这个时候,有三路人想要过去停车。

三条路别离是:转发路、点赞路、赞叹路。

路上的车别离是 why 哥的劳斯莱斯、赵四的布加迪、刘能、谢广坤这对好基友开的法拉利:

这个时候从“点赞路”过去的赵四先开到了,于是停了进去。

门口的停车位显示:残余停车位 2 辆。

刘能、谢广坤到了后发现,刚好还剩下 2 个车位,于是好基友手拉手,一起停了进去。

门口的停车位显示:余下车位 0 辆。

没多久,我也到了,发现没有停车位了,怎么办呢?我只有在门口等一下了。

没一会,赵四办完事了,开着他的布加迪走了。

门口的停车位显示:余下车位 1 辆。

我连忙停进去。

门口的停车位显示:余下车位 0 辆。

下面的代码想要形容的就是这样的一个事件。

然而依据提问者的形容,“在运行时,有时只会执行完线程 A,其线程 B 和线程 C 都静默了。”

在下面这个场景中就是:赵四的布加迪开进去停车后,前面刘能、谢广坤的法拉利和我的劳斯莱斯都停不进去了。

就是这样式儿的:

为什么停不进去呢?他狐疑是死锁了,这个狐疑有点无厘头啊。

咱们先回顾一下死锁的四个必要条件:

  • 互斥条件:一个资源每次只能被一个过程应用,即在一段时间内某资源仅为一个过程所占有。此时若有其余过程申请该资源,则申请过程只能期待。(不满足,还有两个停车位没有用呢。)
  • 申请与放弃条件:过程曾经放弃了至多一个资源,但又提出了新的资源申请,而该资源已被其余过程占有,此时申请过程被阻塞,但对本人已取得的资源放弃不放。(不满足,张三占了一个停车位了,没有提出还要一个停车位的要求,另外的停车位也没有被占用)
  • 不可剥夺条件: 过程所取得的资源在未应用结束之前,不能被其余过程强行夺走,即只能由取得该资源的过程本人来开释。(满足,张三的车不开进去,这个停车位实践上是不会被夺走的)
  • 循环期待条件: 若干过程间造成首尾相接循环期待资源的关系。(不满足,只有我和刘能、谢广坤两拨人在等资源,但没有循环期待的状况。)

这四个条件是死锁的必要条件,必要条件就是说只有有死锁了,这些条件必然全副成立。

而通过剖析,咱们发现没有满足死锁的必要条件。那为什么会呈现这样的景象呢?

咱们先依据下面的场景,本人写一段代码。

本人撸代码

上面的程序基本上是依照下面截图中的示例代码接合下面的故事改的,能够间接复制粘贴:

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

        Integer parkSpace = 3;
        System.out.println("这里有" + parkSpace + "个停车位, 先到先得啊!");
        Semaphore semaphore = new Semaphore(parkSpace, true);

        Thread threadA = new Thread(new ParkCar(1, "布加迪", semaphore), "赵四");
        Thread threadB = new Thread(new ParkCar(2, "法拉利", semaphore), "刘能、谢广坤");
        Thread threadC = new Thread(new ParkCar(1, "劳斯莱斯", semaphore), "why 哥");

        threadA.start();
        threadB.start();
        threadC.start();}
}

class ParkCar implements Runnable {
    
    private int n;
    private String carName;
    private Semaphore semaphore;

    public ParkCar(int n, String carName, Semaphore semaphore) {
        this.n = n;
        this.carName = carName;
        this.semaphore = semaphore;
    }

    @Override
    public void run() {
        try {if (semaphore.availablePermits() < n) {System.out.println(Thread.currentThread().getName() + "来停车, 然而停车位不够了, 等着吧");
            }
            semaphore.acquire(n);
            System.out.println(Thread.currentThread().getName() + "把本人的" + carName + "停进来了, 残余停车位:" + semaphore.availablePermits() + "辆");
            // 模仿停车时长
            int parkTime = ThreadLocalRandom.current().nextInt(1, 6);
            TimeUnit.SECONDS.sleep(parkTime);
            System.out.println(Thread.currentThread().getName() + "把本人的" + carName + "开走了, 停了" + parkTime + "小时");
        } catch (Exception e) {e.printStackTrace();
        } finally {semaphore.release(n);
            System.out.println(Thread.currentThread().getName() + "走后, 残余停车位:" + semaphore.availablePermits() + "辆");
        }
    }
}

运行后的后果如下(因为是多线程环境,运行后果可能不尽相同):

这次这个运行后果和咱们预期的是统一的。并没有线程阻塞的景象。

那为什么之前的代码就会呈现“在运行时,有时只会执行完线程 A,其线程 B 和线程 C 都静默了”这种景象呢?

是道德的沦丧,还是兽性的扭曲?我带大家走进代码:

差别就体现在获取残余通行证的办法上。下面是链接外面的代码,上面是我本人写的代码。

说切实的,链接外面的代码我最开始硬是眼神编译了一分钟,没有看出问题来。

当我真正把代码粘到 IDEA 外面,跑起来后发现当最先执行了 B 线程后,A、C 线程都能够执行。当最先执行 A 线程的时候,B、C 线程就不会执行。

我人都懵逼了,重复剖析,发现这和我认知不一样啊!于是我陷入了深思:

过了一会,保洁大爷过去收垃圾,问我:“hi,小帅哥,你这瓶红牛喝完了吧?我把瓶子收走了啊。”而后瞟了一眼屏幕,指着获取残余许可证的那行代码对我说:“你这个中央办法调用错了哈,你再好好看看办法阐明。”

System.out.println("残余可用许可证:" + semaphore.drainPermits());

说完之后,拍了拍我的肩膀,转身离去。失去巨匠点化,我才豁然开朗。

因为获取残余可用许可证的办法是 drainPermits,所以线程 A 调用实现之后,剩下的许可证为 0,而后执行 release 之后,许可证变为 1。(前面会有对应的办法解释)

这时又是一个偏心锁,所以,如果线程 B 先进去排队了,剩下的许可证不足以让 B 线程运行,它就始终等着。C 线程也就没有机会执行。

把获取残余可用许可证的办法换为 availablePermits 办法后,失常输入:

这真的是一个很小的点。所谓当局者迷旁观者清,就是这个情理。

办法解释

我预计很多不太理解 semaphore 的敌人看完后面这两局部也还是稍微有点懵逼。

没事,所有的纳闷将在这一大节解开。

在下面的测试案例中,咱们只用到了 semaphore 的四个办法:

  • availablePermits:获取残余可用许可证。
  • drainPermits:获取残余可用许可证。
  • release(int n):开释指定数量的许可证。
  • acquire(int n):申请指定数量的许可证。

首先看 availablePermits 和 drainPermits 这个两个办法的差别:

这两个中央的文档形容,有点玩文字游戏的意思了。稍不留神就被带进去了。

你认真看:availablePermits 只是 return 以后可用的许可证数量。而 drainPermits 是 acquires and return,它先全副获取后再返回。

availablePermits 只是看看还有多少许可证,drainPermits 是拿走所有剩下的许可证。

所以在下面的场景下,这两个办法的返回值是一样的,然而外部解决齐全外部不一样:

当我把这个发现汇报给保洁大爷后,大爷微微一笑:“小伙子,要不你去查一下 drainPermits 后面的 drain 的意思?”

查完之后,我留下了英语四级的泪水:

见名知意。同学们,可见英语对编程还是十分重要的。

接下来先看看开释的办法:release。

该办法就是开释指定数量许可证。开释,就意味着许可证的减少。就相似于刘能、谢广坤把他们各自的法拉利从停车位开进去,驶离停车场,这时停车场就会多两个停车位。

下面红框框起来的局部是它的次要逻辑。大家本人看一下,我就不翻译了,大略意思就是开释许可证之后,其余等着用许可证的线程就可以看一下开释之后的许可证数量是否够用,如果够就能够获取许可证,而后运行了。

该办法的精髓在 599 到 602 行的阐明中:

这句话十分要害:说的是执行 release 操作的线程不肯定非得是执行了 acquire 办法的线程

开发人员,须要依据理论场景来保障 semaphore 的正确应用。

release 操作这里,大家都晓得须要放到 finally 代码块外面去执行。然而正是这个认知,是最容易踩坑的中央,而且出了问题还十分不好排查的那种。

放必定是要放在 finally 代码块外面的,只是怎么放,这里有点考究。

我接合下一节的例子和 acquire 办法一起阐明:

acquire 办法次要先关注我红框框起来的局部。

从该办法的源码能够看出,会抛出 InterruptException 异样。记住这点,咱们在下一节,带入场景探讨。

release 使用不当的大坑

咱们还是带入之前停车的场景。假如赵四和我先把车停进去了,这个时候刘能、谢广坤他们来了,发现车位不够了,两个好基友嘛,就等着,非要停在一起

等了一会,咱们始终没进去,门口看车的大爷进去对他们说:“我估摸着你们还得等很长时间,别等了,快走吧。”

于是,他们开车离去。

来,就这个场景,整一段代码:

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

        Integer parkSpace = 3;
        System.out.println("这里有" + parkSpace + "个停车位, 先到先得啊!");
        Semaphore semaphore = new Semaphore(parkSpace, true);

        Thread threadA = new Thread(new ParkCar(1, "布加迪", semaphore), "赵四");
        Thread threadB = new Thread(new ParkCar(2, "法拉利", semaphore), "刘能、谢广坤");
        Thread threadC = new Thread(new ParkCar(1, "劳斯莱斯", semaphore), "why 哥");

        threadA.start();
        threadC.start();
        threadB.start();
        // 模仿大爷劝退
        threadB.interrupt();}
}

class ParkCar implements Runnable {

    private int n;
    private String carName;
    private Semaphore semaphore;

    public ParkCar(int n, String carName, Semaphore semaphore) {
        this.n = n;
        this.carName = carName;
        this.semaphore = semaphore;
    }

    @Override
    public void run() {
        try {if (semaphore.availablePermits() < n) {System.out.println(Thread.currentThread().getName() + "来停车, 然而停车位不够了, 等着吧");
            }
            semaphore.acquire(n);
            System.out.println(Thread.currentThread().getName() + "把本人的" + carName + "停进来了," + "残余停车位:" + semaphore.availablePermits() + "辆");
            // 模仿停车时长
            int parkTime = ThreadLocalRandom.current().nextInt(1, 6);
            TimeUnit.SECONDS.sleep(parkTime);
            System.out.println(Thread.currentThread().getName() + "把本人的" + carName + "开走了, 停了" + parkTime + "小时");
        } catch (InterruptedException e) {System.err.println(Thread.currentThread().getName() + "被门口大爷劝走了。");
        } finally {semaphore.release(n);
            System.out.println(Thread.currentThread().getName() + "走后, 残余停车位:" + semaphore.availablePermits() + "辆");
        }
    }
}

看着代码是没有故障,然而运行起来你会发现,有可能呈现这样的状况:

why 哥走后,残余停车位变成了 5 辆?我是开着劳斯莱斯去给他们开发停车位去了吗?

在往前看日志发现,原来是刘能、谢广坤走后,显示了残余停车位 3 辆。

问题就出在这个中央。

而这个中央对应的代码是这样的:

有没有一点豁然开朗的感觉。

50 行抛出了 InterruptedException,导致明明没有获取到许可证的线程,执行了 release 办法,而该办法导致许可证减少。

在咱们的例子外面就是刘能、谢广坤的车都还没停进去,走的时候门口的显示屏就减少了两个停车位。

这就是坑,就是你代码中的 BUG 埋伏地带。

那么怎么修复呢?

答案曾经跃然纸上了, 这个中央须要 catch 起来,如果呈现中断异样,间接返回:

跑起来,后果也正确,所有车都走了后,停车位还是只有 3 辆:

下面的写法还有一个疑难,如果我刚刚拿到许可证,就被中断了,怎么办?

看源码啊,源码外面有答案的。

抛出 InterruptedException 后,调配给这个线程的所有许可证都会被调配给其余想要获取许可证的线程,就像通过调用 release 办法一样。

加强 release

你剖析下面的问题会发现,导致问题的起因是没有获取到许可证的线程,调用了 release 办法。

我感觉这个设定,就是非常容易踩坑的中央。几乎就是一个大坑!

咱们能够就这个问题,对 release 办法进行加强,只有获取后的线程,能力调用 release 办法。

这一招我是在《Java 高并发编程详解 - 深刻了解并发外围库》外面学到的:

其中的 3.4.4 大节《扩大 Semaphore 加强 release》:

获取许可证的办法被批改成这样了(我只截取其中一个办法),获取胜利后放入到队列外面:

外面的 release 办法批改成这样了,执行之前先看看以后线程是否是在队列外面:

还有一段舒适提醒:

这本书写的还是不错的,举荐给大家。

最初说一句(求关注)

满腹经纶,难免会有纰漏,如果你发现了谬误的中央,还请你留言指出来,我对其加以批改。

感谢您的浏览,我保持原创,非常欢送并感谢您的关注。

我是 why,一个被代码耽搁的文学创作者,不是大佬,然而喜爱分享,是一个又暖又有料的四川好男人

正文完
 0