BUAA_OO2025第二单元总结
疾风知劲草,路遥知马力。
前言
起风了。
上个周末的北京起风了。官方提前预警大狂风,周五晚上不信邪的我和🐝想尝尝风的咸淡。嗯,俩人回寝路上吃了俩嘴沙子。
怎么评价呢?我觉得不算咸口😋,嗓子痒痒的。
我还是蛮喜欢在大风中漫步的感觉的😊,感觉是为数不多和大自然较劲的机会,当然最好不要让我吃到沙子。疾风知劲草,前人道理说的不错,但是我这周迎来了自己的【超级疾风】,期中考试、校园论文截稿、会议论文截稿。笔者想想都头皮发麻,今天好好睡了一个懒觉,也是因为实在起不来了😴。在这里写博客,也算许下一个简单的愿望,希望我的家人、朋友和自己都能做自己生命风暴中的【劲草】。我们不一定终于伟大,但一定忠于自己,任尔东西南北疾风暴风大狂风,当人生千磨万击,请诸君务必坚劲。
下面请看正文,为大家带来多线程电梯的OO单元总结。
壹 同步块与锁
1 同步块的设置
同步块是 Java 中实现线程同步的一种基本方式。它的核心思想是:为一段代码指定一个“锁对象”,任何线程想要执行这段代码,必须先获得这个锁对象的所有权(Monitor Lock)。一次只有一个线程能持有该锁,其他试图获取锁的线程会被阻塞,直到持有锁的线程执行完毕并释放锁。
代码语法如下:
1 | synchronized (lockObject) { |
lockObject
(锁对象)的选择直接决定了同步的范围和行为。常见的选择有:
this
对象.class
对象- 任意指定的对象
在本单元的代码迭代中,笔者主要选择的锁对象是第三种——即任意指定的对象。
其实我们在设置同步块时,需要考量的方面有两个:粒度尽可能细、选择合适的保护资源
那如此看来第三种锁对象的优越性就很明显了,主要的优点也有两个:
- 缓解竞争:如果一个类中有多个需要同步的独立资源,可以为每个资源分配不同的锁对象。这样,访问不同资源的线程就不会互相阻塞,提高了并发性(锁的粒度更细)。
- 清晰性:无论在写还是改代码,总能明确同步的对象。
1 | private void unloadAllPassengersForUpdate() { |
2 锁的选择
在本单元作业中,流行的锁主要就是两种,都是OOlens官方推荐的:synchronized
关键字实现的隐式锁和
ReentrantLock
为代表的显式锁。他们的优劣对比我们用表格对比一下:
锁 | synchronized | ReentrantLock |
---|---|---|
复杂度 | 低,自动释放 | 高,手动释放 |
功能性 | 低,不够灵活 | 高,可中断获取锁、实现公平锁等 |
性能 | 低竞争场景较优 | 极高竞争场景较优 |
在本单元作业中,笔者认为synchronized
的简洁性和自动管理能满足要求,于是沿用至第三次作业,且性能与读写锁确实几乎持平。
3 锁/同步块与处理语句之间的关系
在锁中的处理语句虽然看上去是被保护的对象,但是其实我们上锁是为了保护所谓的“共享的状态与属性”,而同步块则是划定了一个“临界区”,在临界区中的代码可以单独访问或者修改其中的状态与属性。他们不仅保证了共享的状态与属性的变化对各个线程的可见性,还保证了临界区的操作的原子性,使之完整且连贯的执行完其中所有的操作。
如果说可见性可以用关键字volatile
维护,那原子性的维护应该是锁所独有的。而且原子性这个性质强的出人意料😮,举例如下:
1 | volatile int count; |
贰 调度器设计与调度策略
1 调度器设计与交互
尽管第一次作业的要求是直接分配,但是考虑到后期还是要采取不同分配策略,可能还要对比不同策略的性能,在第一次迭代作业中笔者便设计了一个分配策略接口,这让笔者在迭代与测试中感觉很是方便😃。
1 | public interface DispatchStrategy { |
我们一共设计了三种线程(不包括主线程),输入线程、分配器线程与电梯线程,分配器在二者之间完成管理总队列并分配请求给电梯的任务。三种线程构成了两个“生产者-消费者”模式:
- 输入线程是生产者,向总队列中添加请求信息;分配器线程是消费者,从中取出请求信息并分配。
- 分配器线程是生产者,在指派电梯后将请求添加进电梯请求表;各电梯线程是消费者,从各自的请求表中取出请求。
2 调度策略
第一次作业直接分配指定的电梯,各电梯采用LOOK策略各自运作。(祖传的策略配方
第二次作业起,就正式进入了调度策略的构建了。最初想要综合各项指标去设计,但是后来发现收益不大的同时,难度却是大得夸张😭。最后采用的是一个简易的打分策略,因为没有时间和能力去调参。主要考虑了两个指标:ETA(预计用时)与容量惩罚。前者其实类似于影子电梯,而后者是出于另一个奇怪设计的考虑。因为笔者在第一次作业就设计了踢人策略,即当优先级较高的人想要乘梯,会将优先级较低的人踢出去😈。虽然在算加权时间上提高了性能,但是可能会导致耗电量增加。因此在后续迭代的分配中,笔者的想法是:除非竞争太过激烈,所有电梯都几近满员,否则尽量在分配的时候就减少踢人的概率。因此设立了第二项指标——容量惩罚,旨在给接近满员的电梯加惩罚分数,某种意义上也有点均匀分配的意思。
叁 宏观回顾:架构分析与总结
1 基于UML图的迭代分析
这次整体迭代的内容主要集中在Elevator
内部,整体架构保持稳定,请看最后一次迭代后的UML类图:
因为整体架构变化不大,就不画三张图了,整体稳定地采用“生产者-消费者”架构模式。第六次作业新增的是分配策略以及回调接口、第七次作业新增换乘层。
如类图所示,我们的设计可以对电梯改造进行扩展,修改运行楼层、速度以及容量等等,同时策略接口也允许我们对多策略进行扩展,甚至可以随着竞争压力大小切换策略。最后,我们为电梯设计的多模式,可以相对轻松地应对类似临时调度与更新的需求,或者对电梯进行还原。
UML协作图如下:
2 识别稳定与易变
首先,毋庸置疑的是总的流水线架构始终保持稳定,不随着迭代的变化而修改,变化的主要是线程内部的指令。其次,在我们处理请求的时候,我们发现无论是哪种请求,它都是Request
的子类,第一次迭代的时候还不明确Request
存在的意义,后来才能理解,是它为我们提供了各种请求的“数据结构”,并在共性特征中以父类的方式保持各种请求的统一。
与之相对的,实现的线程运行细节则会跟随用户需求发生变化,包括临时调度、更新,甚至调度策略也是根据用户是否指定电梯来更改的,他们相对来说就是易变的。
大结构稳定,小细节易变,这完全符合我们做项目迭代的完美状况。
肆 特别讨论:双轿厢问题
学长巧妙的TransFloor
类的设计也是被我用上了😊,通过此类对换乘层进行管理。首先从面向对象的角度考虑,如果你要在电梯内部运行的时候,得知另一个电梯的运行状态,那可太影响类内部的封装性了。其次,出于对对称性的考虑,应该要设计一个类出来,保证它对两个电梯的态度都是"公平对等"的。
那具体如何避免碰撞呢?说白了就是:
1 | public synchronized boolean Occupy() { // 电梯A进去前要问问:“换乘层有没有梯?” |
此外,便是要实现双轿厢同步开始改造。其实同理,但是我们引入两个布尔值,在一个方法里完成互斥:
1 | public synchronized void synchronizeElevators() { |
伍 BUG&DEBUG
最折磨的一集😈
先说DEBUG,不会用多线程调试,也有大佬说多线程调试给到的信息过少,根本无法定位错误。因此果断采用Print大法,其实不知道哪位先提出的,就是统一设置一个debug开关,可以大幅减少工作量。我在公共定义类中的代码如下:
1 | private static final boolean debug = false; |
再说说BUG,这个单元的bug主要都围绕着踢人策略。第一次作业中就是为了实现踢人导致的粗心错误,具体而言即当一个优先级较高的请求将另一个优先级较低的请求踢掉后,没有输入上电梯的信息😭,因此导致了上电梯但没有记录的鬼魂事件👻。
对于第二、三次作业陆续出现的临时操作:SCHE
与UPDATE
,它们一方面在运行时不能接人,另一方面要把电梯内外的请求清空。对于不能接人这一要求,我在分配器线程与电梯线程中通过延迟输出RECEIVE的方式解决;而后者中清空请求列表相对棘手,为了保持低耦合度,电梯类内部不应该能洞察到外部总列表的信息,因此把电梯列表里的请求返还给总请求列表这里用到了回调接口,而没有直接让电梯能在内部对总请求队列进行修改。
第二次作业中第一次尝试写回调接口,最后出了很多线程问题,太赶时间导致中测甚至爆了两个点。🤦
1 | // 函数接口声明 |
陆 心得体会
1 线程安全
这个单元的主题就是多线程编程,其中最重要的就是保护线程的安全。我在编写代码的时候会有意识地尽可能少上锁,因为在第五次作业debug时曾疯狂加锁,发现容易导致死锁等问题,程序甚至无法正常运行。因此在第六次作业分配策略中计算ETA时,我没有选择像传统影子电梯为每个电梯都挂锁,而是选择为电梯拍下快照,第一直觉是误差增大,但是想想影子电梯也会有一定的误差后,我就觉得这点误差可以接收了。我只需要在拍快照的时候维护电梯线程的数据安全,真正调用分配策略的时候,电梯就可以正常运行了。少用一些同步块,或者有意识减小同步块的颗粒度都是可以保证正确的同时提升性能的。另外就是同步块到底同步的是谁,能不能锁住,在debug的时候要仔细推敲,避免被小bug硬控🤬。
2 层次化设计
笨人没有用到太多的设计模式,但是紧紧抓紧了“生产者-消费者”的大腿,构建出来的“流水线”设计让人在写代码和改代码的时候都可以思路清晰地完成。更重要的是在debug时,可以通过输出调试法,一步一步定位出错的环节,有一种类似顺藤摸瓜的感觉。峰回路转,但是你确信、也确实一定能找到问题所在。
后记
交完了OO单元总结,后面的话就是想在网站上说的了(bushi)。也没什么不能说的,我感觉我有一种莫名的慢热,当我写总结的时候,我才对OO展现出了比较浓厚的兴趣🧐,希望这段时间把学习状态控制好,最主要是让我睡个好觉呗😴,已经连续快一周没睡好了。
好的!言尽于此,周末也快要告一段落了,抓紧去玩一会!🤩
If you like this blog or find it useful for you, you are welcome to comment on it. You are also welcome to share this blog, so that more people can participate in it. If the images used in the blog infringe your copyright, please contact the author to delete them. Thank you !