共享模型之管程(四)

news2024/11/24 20:09:31

1.wait/notify

1.1.为什么需要wait?

小故事:

①.假设多个用户(线程)都需要进入房间使用算盘(CPU)进行计算工作,但是为了保证计算过程中的安全,老王设计了一把锁(Synchronized),每次只允许一个用户(线程)拿到钥匙进入房间(成为Owner线程);

②.小南(线程)费了九牛二虎之力,抢到了锁,进入到房间(成为Owner线程),但是在工作过程中由于条件不满足,小南不能继续进行计算,此时小南就需要等待.但小南如果一直占用着锁,其它人就得一直阻塞,效率太低;
在这里插入图片描述

③.于是老王单开了一间休息室(调用wait方法),让小南到休息室(WaitSet)等着去了,此时锁就被释放开,其它人可以由老王随机安排进屋(不满足条件的线程不会影响其他线程的运行!);

④.直到小M将烟送来,大叫一声"你的烟到了" (调用notify方法);
在这里插入图片描述

⑤.于是小南就离开了休息室,(为了公平起见)重新进入竞争锁的队列(EntryList),进行下一轮锁的竞争;
在这里插入图片描述

1.2.原理

在这里插入图片描述

①.Owner线程(获取对象关联的Monitor对象的线程,即获取Monitor锁的线程)发现条件不满足,为了不占用锁资源,它会调用wait()方法,即可进入WaitSet变为WAITING状态(锁会被释放,同时唤醒EntryList等待队列中处于BLOCKED阻塞状态的线程,然后这些线程竞争锁);

②.BLOCKED和WAITING状态的线程都处于阻塞状态,不占用CPU时间片;

③.BLOCKED状态的线程会在Owner线程释放锁时自动被唤醒;而WAITING状态的线程会在Owner线程调用notify或notifyAll方法时才能唤醒(注意此时的Owner线程已经不是之前的Owner线程),而且唤醒后并不意味着立刻获得锁,仍需进入EntryList等待队列中变成BLOCKED状态,等待再次被唤醒,然后参与锁竞争;

当执行其他线程中的"obj.notifyAll()或者obj.notify()"这一行代码之后,WaitSet中的WAITING状态的线程会被唤醒尝试获取对象锁,但是此时其他线程Synchronized(obj)代码还没有执行完毕,也就是说对象锁还没有释放,此时被唤醒的线程获取对象锁失败,然后这些线程会进入EntryList等待队列中变成BLOCKED状态,等待被唤醒,重新竞争对象锁;

1.3.API介绍

1>.obj.wait(long timeout): 让进入object监视器的线程(不满足条件无法继续运行的Owner线程)到waitSet等待;

①.如果wait()方法没有添加参数或者参数为’0’,那么就表示该线程会一直/无限制等待下去,直到notify/notifyAll为止;

②.如果wait()方法加了参数(/超时时间),那么就表示该线程只会等待指定的时间,如果超过这个时间还没有被唤醒则自动结束等待,继续执行后面的代码;如果在指定的等待时间之内提前被唤醒了,那么就可以提前执行而不需要等到指定时间到达;

2>.obj.notify(): 在object上正在waitSet等待的线程中挑一个唤醒;

3>.obj.notifyAll(): 让object上正在waitSet等待的线程全部唤醒;

它们都是线程之间进行协作/通信的手段,都属于Object对象的方法.必须获得此对象的锁(必须成为对象关联的Monitor对象的所有者),才能调用这几个方法(即必须是在synchronized代码块中才能调用这几个方法),否则会出现'IllegalMonitorStateException'异常!!!

1.4.案例

@Slf4j
public class TestNotifyOrNotifyAll {
    //对象锁
    static final Object OBJ = new Object();
    public static void main(String[] args) throws InterruptedException {
        new Thread(() -> {
            synchronized (OBJ) {
                log.info("线程t1开始执行....");
                try {
                    //必须获取对象的锁,才能调用相关的方法,否则会出现'IllegalMonitorStateException'异常;
                    //让线程在obj上一直等待下去
                    OBJ.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                log.info("线程t1执行其它代码....");
            }
        }, "t1").start();

        new Thread(() -> {
            synchronized (OBJ) {
                log.info("线程t2开始执行....");
                try {
                    //让线程在obj上一直等待下去
                    OBJ.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                log.info("线程t2执行其它代码....");
            }
        }, "t2").start();

        // 主线程两秒后执行
        TimeUnit.SECONDS.sleep(2);

        log.info("main线程唤醒obj上其它线程");
        synchronized (OBJ) {
            //唤醒obj上一个线程,另外一个线程会一直等待,导致程序一直处于运行中,无法正常停止!
            //OBJ.notify();
            //唤醒obj上所有等待线程
            OBJ.notifyAll();
        }
    }
}

在这里插入图片描述

1.5.sleep(long n)和wait(long n)的区别

1>.sleep是Thread中的(静态)方法,而wait是Object的(原生)方法;

2>.sleep不需要强制和synchronized配合使用,但wait必须和synchronized一起用,而且是在synchronized代码块内部使用;

3>.sleep在睡眠的同时不会释放对象锁的,但wait在等待的时候会释放对象锁;

4>.它们都会让线程的状态变成TIMED_WAITING阻塞;

1.6.wait/notify的正确姿势

1>.示例代码

@Slf4j
public class TestNotifyOrWait {
    //注意:锁对象最好使用"final"关键字来修饰,可以防止其他子类对其修改!!
    static final Object room = new Object();
    static boolean hasCigarette = false;

    public static void main(String[] args) throws InterruptedException {
        new Thread(() -> {
            synchronized (room) {
                log.info("有烟没?[{}]", hasCigarette);
                if (!hasCigarette) {
                    log.info("没烟,先歇会!");
                    try {
                        //小南线程获取到对象锁,但是由于条件不满足,无法继续执行
                        //小南线程必须睡足2s后才能醒来,就算烟提前送到,也无法立刻醒来
                        //释放CPU时间片,但是不释放对象锁;
                        Thread.sleep(2000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }

                log.info("有烟没?[{}]", hasCigarette);
                if (hasCigarette) {
                    log.info("可以开始干活了");
                }
            }
        }, "小南").start();

        for (int i = 0; i < 5; i++) {
            new Thread(() -> {
                //由于对象锁一直被小南线程占用,因此这里多个线程无法获取到对象锁而进入EntryList等待;
                //等到小南线程执行完synchronized同步代码块中的代码释放了对象锁,唤醒这些线程,他们才可以执行;
                synchronized (room) {
                    log.info("可以开始干活了");
                }
            }, "其它人").start();
        }

        //main线程阻塞
        Thread.sleep(1000);

        //送烟的线程修改小南线程的执行条件
        new Thread(() -> {
            //这里能不能加 synchronized(room)?
            //如果在这里加上synchronized同步块,那么该送烟线程必须等到小南线程执行完毕释放对象锁之后才有可能修改小南线程的执行条件,可是此时小南线程已经运行完毕(任务没有做);
            hasCigarette = true;
            log.info("烟到了噢!");
        }, "送烟的").start();
    }
}

在这里插入图片描述
分析:

①.其它干活的线程在小南线程执行完释放对象锁之前都要一直阻塞,效率太低;

②.小南线程必须睡足2s后才能醒来,就算烟提前送到,也无法立刻醒来;

③.加了synchronized (room)后,就好比小南在里面反锁了门睡觉,烟根本没法送进门,main线程没加synchronized就好像main线程是翻窗户进来的;

③.加了synchronized (room)后,就好比小南在里面反锁了门睡觉,烟根本没法送进门,main线程没加synchronized就好像main线程是翻窗户进来的;

2>.示例代码

@Slf4j
public class TestNotifyOrWait2 {

    //注意:锁对象最好使用"final"关键字来修饰,可以防止其他子类对其修改!!
    static final Object room = new Object();
    static boolean hasCigarette = false;

    public static void main(String[] args) throws InterruptedException {
        new Thread(() -> {
            synchronized (room) {
                log.info("有烟没?[{}]", hasCigarette);
                if (!hasCigarette) {
                    log.info("没烟,先歇会!");
                    try {
                        //小南线程获取到对象锁,但是条件不满足,进入WaitSet中变成WAITING状态
                        //如果在等待期间提前被唤醒,那可以提前执行(进入到EntryList中)
                        //如果在等待时间到达之后还没有被唤醒,也会自动结束等待,继续执行(进入到EntryList中)
                        //在等待期间小南线程会释放对象锁
                        room.wait(2000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                log.info("有烟没?[{}]", hasCigarette);
                if (hasCigarette) {
                    log.info("可以开始干活了");
                }
            }
        }, "小南").start();

        for (int i = 0; i < 5; i++) {
            new Thread(() -> {
                //这里多个其他线程可以拿到对象锁,执行自己的任务
                synchronized (room) {
                    log.info("可以开始干活了");
                }
            }, "其它人").start();
        }

        Thread.sleep(1000);

        new Thread(() -> {
            //这里的送烟线程也可以拿到对象锁,修改小南线程的执行条件
            //同时还会唤醒WaitSet中处于WAITING状态的线程(小南线程)
            //之后小南线程进入到EntryList等待队列中进行BLOCKED阻塞,等待被唤醒;
            synchronized (room) {
                hasCigarette = true;
                log.info("烟到了噢!");
                room.notify();
            }
        }, "送烟的").start();
    }
}

在这里插入图片描述
分析:

①.解决了其它干活的线程阻塞的问题;

②.但如果有其它线程也在等待条件,那么送烟线程会不会把其他WAITING等待中的线程唤醒?

3>.示例代码

@Slf4j
public class TestWaitOrNotify {
    //注意:锁对象最好使用"final"关键字来修饰,可以防止其他子类对其修改!!
    static final Object room = new Object();
    static boolean hasCigarette = false;
    static boolean hasTakeout = false;

    public static void main(String[] args) throws InterruptedException {
        new Thread(() -> {
            synchronized (room) {
                log.info("有烟没?[{}]", hasCigarette);
                if (!hasCigarette) {
                    log.info("没烟,先歇会!");
                    try {
                        //小南线程进入WAITING状态,释放锁
                        room.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                log.info("有烟没?[{}]", hasCigarette);
                if (hasCigarette) {
                    log.info("可以开始干活了");
                } else {
                    log.info("没干成活...");
                }
            }
        }, "小南").start();
        
        new Thread(() -> {
            synchronized (room) {
                log.info("外卖送到没?[{}]", hasTakeout);
                if (!hasTakeout) {
                    log.info("没外卖,先歇会!");
                    try {
                        //小女线程进入WAITING状态,释放锁
                        room.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                log.info("外卖送到没?[{}]", hasTakeout);
                if (hasTakeout) {
                    log.info("可以开始干活了");
                } else {
                    log.info("没干成活...");
                }
            }
        }, "小女").start();
        

        TimeUnit.SECONDS.sleep(1);
        
        new Thread(() -> {
            synchronized (room) {
                hasTakeout = true;
                log.info("外卖到了噢!");
                //随机唤醒WaitSet中的一个线程
                //注意:有可能这个送外卖的线程唤醒的是小南线程,但是小南线程需要的烟不是外卖(虚假唤醒),而且另外一个线程
                //没有被唤醒.就会一直处于WAITING状态,最终导致程序无法正常退出;
                room.notify();
            }
        }, "送外卖的").start();
    }
}

在这里插入图片描述
分析:

①.notify只能随机唤醒一个WaitSet中的线程,这时如果还有其它线程也在等待,那么notify就可能唤醒不了正确的线程,称之为"虚假唤醒";

②.解决方法: 改为notifyAll;

4>.示例代码

@Slf4j
public class TestWaitOrNotify {
    //注意:锁对象最好使用"final"关键字来修饰,可以防止其他子类对其修改!!
    static final Object room = new Object();
    static boolean hasCigarette = false;
    static boolean hasTakeout = false;

    public static void main(String[] args) throws InterruptedException {
        new Thread(() -> {
            synchronized (room) {
                log.info("有烟没?[{}]", hasCigarette);
                if (!hasCigarette) {
                    log.info("没烟,先歇会!");
                    try {
                        //小南线程进入WAITING状态,释放锁
                        room.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                log.info("有烟没?[{}]", hasCigarette);
                if (hasCigarette) {
                    log.info("可以开始干活了");
                } else {
                    log.info("没干成活...");
                }
            }
        }, "小南").start();
        
        new Thread(() -> {
            synchronized (room) {
                log.info("外卖送到没?[{}]", hasTakeout);
                if (!hasTakeout) {
                    log.info("没外卖,先歇会!");
                    try {
                        //小女线程进入WAITING状态,释放锁
                        room.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                log.info("外卖送到没?[{}]", hasTakeout);
                if (hasTakeout) {
                    log.info("可以开始干活了");
                } else {
                    log.info("没干成活...");
                }
            }
        }, "小女").start();
        

        TimeUnit.SECONDS.sleep(1);
        
        new Thread(() -> {
            synchronized (room) {
                hasTakeout = true;
                log.info("外卖到了噢!");
                //随机唤醒WaitSet中的一个线程
                //注意:有可能这个送外卖的线程唤醒的是小南线程,但是小南线程需要的烟不是外卖,而且另外一个线程
                //没有被唤醒.就会一直处于WAITING状态,最终导致程序无法正常退出;
                //room.notify();
                //唤醒WaitSet中所有的线程
                room.notifyAll();
            }
        }, "送外卖的").start();
    }
}

在这里插入图片描述
分析:

①.用notifyAll仅解决某个线程的唤醒问题,但使用 if + wait判断仅有一次机会,一旦条件不成立,就没有重新判断的机会了;

②.解决方法: 用while + wait,当条件不成立,再次wait;

5>.示例代码

@Slf4j
public class TestWaitOrNotify {
    //注意:锁对象最好使用"final"关键字来修饰,可以防止其他子类对其修改!!
    static final Object room = new Object();
    static boolean hasCigarette = false;
    static boolean hasTakeout = false;

    public static void main(String[] args) throws InterruptedException {
        new Thread(() -> {
            synchronized (room) {
                log.info("有烟没?[{}]", hasCigarette);
                while (!hasCigarette) {
                    log.info("没烟,先歇会!");
                    try {
                        //小南线程进入WAITING状态,释放锁
                        room.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                log.info("有烟没?[{}]", hasCigarette);
                if (hasCigarette) {
                    log.info("可以开始干活了");
                } else {
                    log.info("没干成活...");
                }
            }
        }, "小南").start();
        
        new Thread(() -> {
            synchronized (room) {
                log.info("外卖送到没?[{}]", hasTakeout);
                while (!hasTakeout) {
                    log.info("没外卖,先歇会!");
                    try {
                        //小女线程进入WAITING状态,释放锁
                        room.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                log.info("外卖送到没?[{}]", hasTakeout);
                if (hasTakeout) {
                    log.info("可以开始干活了");
                } else {
                    log.info("没干成活...");
                }
            }
        }, "小女").start();
        

        TimeUnit.SECONDS.sleep(1);
        
        new Thread(() -> {
            synchronized (room) {
                hasTakeout = true;
                log.info("外卖到了噢!");
                //随机唤醒WaitSet中的一个线程
                //注意:有可能这个送外卖的线程唤醒的是小南线程,但是小南线程需要的烟不是外卖,而且另外一个线程
                //没有被唤醒.就会一直处于WAITING状态,最终导致程序无法正常退出;
                //room.notify();
                room.notifyAll();
            }
        }, "送外卖的").start();
    }
}

在这里插入图片描述
分析:

把if改成while可以防止虚假唤醒!!!

小总结: 正确使用wait/notify的套路

//某一个线程
synchronized(对象锁) {
 while(条件不成立) {
   对象锁.wait();
 }
 // 干活
}

//另一个线程
synchronized(同一个对象锁) {
  同一个对象锁.notifyAll();
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/141832.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【Docker】搭建Zookeeper集群

【Docker】搭建Zookeeper集群 下载镜像 docker pull zookeeper:3.5.8wy:study wy$ docker pull zookeeper:3.5.8 3.5.8: Pulling from library/zookeeperDigest: sha256:12af523731cbe390f5332d6c1e254f1d56c734a786910d5582653445a5cee299 Status: Downloaded newer image f…

Allegro174版本新功能介绍之动态铜皮对单独层面参数设置

Allegro174版本新功能介绍之动态铜皮对单独层面参数设置 Allegro升级到了174版本的时候,可以支持动态铜皮对单独的层面进行参数设置,如下图 具体操作如下 在低版本166以及172的时候,只有Global Dynamic Shape Parameter设置,如下图,只有全局的铜皮参数设置升级到了174时候…

WMS智能仓储管理系统源码 SpringMVC物流仓库管理系统源码

淘源码&#xff1a;国内知名的源码免费下载平台 需要源码学习可私信我。 系统介绍&#xff1a; 基于SpringMVCHibernatMinidao&#xff08;类Mybatis&#xff09;Easyui&#xff08;UI库&#xff09; Jquery Boostrap Ehcache Redis Ztree等基础架构开发的物流仓库管理系…

人脸识别:我是如何工作的?

任何自动人脸识别过程都必须考虑导致其复杂性的几个因素&#xff0c;因为人脸是一个动态实体&#xff0c;在多个因素的影响下不断变化&#xff0c;例如光照、姿势、年龄……这三个参数中的任何一个的变化都会导致同一个人的两幅图像之间的误差值大于不同个体的两幅图像之间的误…

分享136个PHP源码,总有一款适合您

PHP源码 分享136个PHP源码&#xff0c;总有一款适合您 136个PHP源码下载链接&#xff1a;https://pan.baidu.com/s/1A5sR357dh_SlS7pu33lW1Q?pwdkzgn 提取码&#xff1a;kzgn import os# 查找指定文件夹下所有相同名称的文件 def search_file(dirPath, fileName):dirs os…

红中私教-文件上传漏洞DVWA靶场实战(浅析)

前言 &#x1f340;作者简介&#xff1a;被吉师散养、喜欢前端、学过后端、练过CTF、玩过DOS、不喜欢java的不知名学生。 &#x1f341;个人主页&#xff1a;红中 首先呢&#xff0c;针对于文件上传漏洞 这个漏洞为什么存在&#xff1f;目的是什么&#xff1f;我们为什幺要攻击…

开发环境和测试环境共用Eureka

问题描述 在开发过程中会遇到一种情况&#xff0c;那就是只需要修改一个服务&#xff0c;但是这个服务依赖了其他的3个服务&#xff0c;导致开发人员在本地也要启动其他的3个服务&#xff0c;还要启动一个Eureka注册中心。问题显而易见&#xff0c;在依赖过多的情况下&#xf…

一种多维数据库的数据事务专利解读

什么是事务&#xff1f; 事务是数据库系统中的核心机制。我们要理解下事务概念&#xff1a;什么是事务呢&#xff1f;事务是并发控制的单位&#xff0c;是用户定义的一个操作序列。有四个特性(ACID)&#xff1a; 原子性(Atomicity)&#xff1a; 事务是数据库的逻辑工作单位&…

使用 VSCode 开发的必备插件,你都安装了吗?

0️⃣前言 VSCode是由微软研发的一款免费、开源的跨平台代码编辑器&#xff0c;目前是前端开发使用最多的一款软件开发工具。 因为每个开发者所接触项目、所有技术不同, 用到的插件不同, 但总有几个插件基本是必备的, 以下就给出一些插件推荐&#xff0c;希望能给大家一些参考。…

腾讯会议发布录屏工具“会记”,让云端视频协作随用随录、随享随看

随着云端协同成为新常态&#xff0c;企业和组织沟通的形式也在不断丰富。1月5日&#xff0c;腾讯会议发布云端录屏工具“会记”&#xff0c; 用户在腾讯会议中就能自由使用人像、屏幕、声音、窗口等多种组合方式进行录制&#xff0c;录制完成后视频将自动上传至云端&#xff0c…

Centos7.9安装WebLogic详细步骤

目录 一、weblogic下载 二、准备环境 三、创建用户和组 四、安装jdk 安装 五、安装WebLogic 1、使用root用户创建目录/opt/weblogic并授权 2.创建 oraInst.loc 文件 3、创建wls.rsp 响应文件 4、安装weblogic 5、静默创建域 六、启动weblogic 一、weblogic下载 直…

程序员面试中一面、二面、三面有什么区别?

很多公司面试都分一面、二面、三面甚至更多&#xff0c;大家可能会好奇&#xff0c;为什么要面这么多面&#xff0c;每一面又有啥区别呢&#xff1f; 首先我来回答下为什么要这么多面&#xff0c;最核心的是最后3点&#xff1a; 如果光是一个人面&#xff0c;担心会看走眼&…

解决IDEA中ctrl+shift+f快捷键搜索没反应的问题

文章目录0写在前面1 问题解决1.1 直接切换英文输入法1.2 win10 输入法1.3 搜狗输入法1.4 其他2 写在末尾0写在前面 今天想使用《在文件中查询》功能&#xff0c;使用ctrlshiftf进行搜索的时候&#xff0c;疯狂的按组合键一直没用。最后用手点的…… 原因是IDEA的快捷键与输入…

【 java 集合】List接口常用方法总结

&#x1f4cb; 个人简介 &#x1f496; 作者简介&#xff1a;大家好&#xff0c;我是阿牛&#xff0c;全栈领域优质创作者。&#x1f61c;&#x1f4dd; 个人主页&#xff1a;馆主阿牛&#x1f525;&#x1f389; 支持我&#xff1a;点赞&#x1f44d;收藏⭐️留言&#x1f4d…

五、条件构造器和常用接口

文章目录五、条件构造器和常用接口1、wapper介绍2、QueryWrapper2.1 例1&#xff1a;组装查询条件2.2 例2&#xff1a;组装排序条件2.3 例3&#xff1a;组装删除条件2.4 例4&#xff1a;条件的优先级2.5 例5&#xff1a;组装select子句2.6 例6&#xff1a;实现子查询3、UpdateW…

基于opencv的数字识别系统

一、目的 想要实现的功能&#xff1a;帮助我们在泵中扫描燃油&#xff0c;并在应用程序中输入燃油信息。 所需技术&#xff1a;①python程序对于拍摄的汽油泵的图像&#xff0c;尝试从中读取数字。——opencv实现。②先使用python对其进行原型设计&#xff0c;然后将代码转换…

TiDB分布式数据库部署与安装详解

TiUP 是 TiDB 4.0 版本引入的集群运维工具&#xff0c;TiUP cluster 是 TiUP 提供的使用 Golang 编写的集群管理组件&#xff0c;通过 TiUP cluster 组件就可以进行日常的运维工作&#xff0c;包括部署、启动、关闭、销毁、弹性扩缩容、升级 TiDB 集群&#xff0c;以及管理 TiD…

EMQX Enterprise 4.4.124.4.13 发布:集群负载重平衡、TDengine 3.0 适配以及子表批量插入

我们很高兴地告诉大家&#xff0c;EMQX Enterprise 4.4.12 以及 4.4.13 版本正式发布&#xff01; 在本次发布中&#xff0c;我们带来了集群负载重平衡与节点疏散功能为运维人员提供更灵活的集群管理方式&#xff0c;适配了 TDengine 3.0 版本并新增分表批量插入功能&#xff…

ECMAScript 2022 中的新特性!

新钛云服已累计为您分享718篇技术干货本文盘点ECMAScript 2022 中的新特性&#xff0c;包括顶级等待、RegExp 匹配索引、新的公共和私有类字段等。一、公共和私有实例字段最新的 ES13 规范允许我们将成员字段内联定义为类主体的一部分&#xff0c;我们可以使用#来表示私有字段。…

Python打包(问题记录,带解决)

引言 文章用于测试在Python3.8的版本&#xff0c;打包Obspy地震包&#xff0c;最后集成到PyQt5上。 部署或冻结应用程序是 Python 项目的重要组成部分&#xff0c; 这意味着捆绑所有必需的资源&#xff0c;以便应用程序找到它需要的一切 能够在客户端计算机上运行。 但是&…