我靠!Semaphore里面居然有这么一个大坑! (2)

简介: 我靠!Semaphore里面居然有这么一个大坑! (2)

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


我们先回忆一下死锁的四个必要条件:


  • 互斥条件:一个资源每次只能被一个进程使用,即在一段时间内某资源仅为一个进程所占有。此时若有其他进程请求该资源,则请求进程只能等待。(不满足,还有两个停车位没有用呢。)


  • 请求与保持条件:进程已经保持了至少一个资源,但又提出了新的资源请求,而该资源已被其他进程占有,此时请求进程被阻塞,但对自己已获得的资源保持不放。(不满足,张三占了一个停车位了,没有提出还要一个停车位的要求,另外的停车位也没有被占用)


  • 不可剥夺条件:进程所获得的资源在未使用完毕之前,不能被其他进程强行夺走,即只能由获得该资源的进程自己来释放。(满足,张三的车不开出来,这个停车位理论上是不会被夺走的)


  • 循环等待条件: 若干进程间形成首尾相接循环等待资源的关系。(不满足,只有我和刘能、谢广坤两拨人在等资源,但没有循环等待的情况。)


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


而经过分析,我们发现没有满足死锁的必要条件。那为什么会出现这样的现象呢?


我们先根据上面的场景,自己写一段代码。



自己撸代码


下面的程序基本上是按照上面截图中的示例代码接合上面的故事改的,可以直接复制粘贴:


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() + "辆");
        }
    }
}



image.png


这次这个运行结果和我们预期的是一致的。并没有线程阻塞的现象。


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


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


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


image.png


差异就体现在获取剩余通行证的方法上。上面是链接里面的代码,下面是我自己写的代码。


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


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


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


image.png


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


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


说完之后,拍了拍我的肩膀,转身离去。得到大师点化,我才恍然大悟。



image.png


由于获取剩余可用许可证的方法是 drainPermits,所以线程 A 调用完成之后,剩下的许可证为0,然后执行 release 之后,许可证变为 1。(后面会有对应的方法解释)


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


把获取剩余可用许可证的方法换为 availablePermits 方法后,正常输出:


image.png


方法解释


我估计很多不太了解 semaphore 的朋友看完前面这两部分也还是略微有点懵逼。


没事,所有的疑惑将在这一小节解开。


在上面的测试案例中,我们只用到了 semaphore 的四个方法:


  • availablePermits:获取剩余可用许可证。


  • drainPermits :获取剩余可用许可证。


  • release(int n):释放指定数量的许可证。


  • acquire(int n):申请指定数量的许可证。


首先看 availablePermits 和 drainPermits 这个两个方法的差异:



image.png

目录
相关文章
|
存储 Python 容器
python中的h5py开源库的使用
python中的h5py开源库的使用
178 1
|
关系型数据库 MySQL Linux
【Linux】安装与配置虚拟机及虚拟机服务器坏境配置与连接
【Linux】安装与配置虚拟机及虚拟机服务器坏境配置与连接
232 0
【Linux】安装与配置虚拟机及虚拟机服务器坏境配置与连接
|
网络协议 应用服务中间件 测试技术
|
4天前
|
人工智能 JavaScript 测试技术
Qwen3-Coder入门教程|10分钟搞定安装配置
Qwen3-Coder 挑战赛简介:无论你是编程小白还是办公达人,都能通过本教程快速上手 Qwen-Code CLI,利用 AI 轻松实现代码编写、文档处理等任务。内容涵盖 API 配置、CLI 安装及多种实用案例,助你提升效率,体验智能编码的乐趣。
354 105
|
5天前
|
JSON fastjson Java
FastJson 完全学习指南(初学者从零入门)
摘要:本文是FastJson的入门学习指南,主要内容包括: JSON基础:介绍JSON格式特点、键值对规则、数组和对象格式,以及嵌套结构的访问方式。FastJson是阿里巴巴开源的高性能JSON解析库,具有速度快、功能全、使用简单等优势,并介绍如何引入依赖,如何替换Springboot默认的JackJson。 核心API: 序列化:将Java对象转换为JSON字符串,演示对象、List和Map的序列化方法; 反序列化:将JSON字符串转回Java对象,展示基本对象转换方法;
|
5天前
|
缓存 JavaScript 前端开发
JavaScript 的三种引入方法详解
在网页开发中,JavaScript 可通过内联、内部脚本和外部脚本三种方式引入 HTML 文件,各具适用场景。本文详解其用法并附完整示例代码,帮助开发者根据项目需求选择合适的方式,提升代码维护性与开发效率。
201 110
|
6天前
|
Android开发 开发者 Windows
这是我设计的一种不关机,然后改造操作系统的软件设计思路2.0版本
本文介绍了在不重启系统的情况下实现操作系统改造的两种方案。第一种方案通过SLFM Recovery模式,在独立于操作系统的最高权限环境下完成系统更新与改造,并支持断电恢复与失败回滚。第二种方案采用多分区机制,通过SLFM套件在独立分区中完成系统改造,适用于可中断与不可中断服务场景,确保系统更新过程的安全与稳定。
232 132