BUAA OO第二单元总结

陈耕-23373262 2025-04-16 14:48:52

一、第一次作业

1、作业要求

以大楼的电梯为背景,建立属于自己的电梯系统。
要求:

  • 乘客搭乘电梯前自行填写目的楼层以及搭乘电梯,同时给出乘客的优先级。自行设计电梯运行策略,来实现电梯最终能将所有乘客送到目的地。同时考虑所有电梯运行时间,乘客按照优先级的加权等待时间,电梯耗电量。

2、架构分析

  • 生产者-消费者模型
    生产者-消费者模型用于解决多线程环境下的生产者和消费者问题。在生产者-消费者模型中,生产者负责产生产品,消费者负责消费产品。同时设计一个托盘,生产者生产得到的产品放入托盘,消费者从托盘中取出产品进行消费。
  • 锁的设置
    在生产者-消费者模型中,生产者和消费者分别代表两个线程,托盘表示两者之间的共享对象。多线程对于共享对象的读写需要考虑同步互斥问题;对共享对象进行加锁,可以实现同一时刻仅有一个线程对共享对象进行读写操作;调用wait()以及notify()方法可以实现线程的等待与唤醒,从而解决同步问题。

线程设计

在本次作业中,总共设计了三个线程:输入线程、电梯调度器线程以及电梯线程。

  • 1、输入线程与调度器共享requestTable类,输入线程负责将乘客的请求放入requestTable中,调度器线程负责从requestTable中取出乘客的请求。

    • 在解决此线程安全问题中,主要是对requestTable中每个方法进行上锁,构建一个线程安全类,来保证线程安全。
  • 2、调度器和电梯共享等待队列waitQueue对象,调度器将乘客放入waitQueue中,电梯根据自身运行策略根据不同楼层的等待队列进行运行以及捎带决策。

    • 在解决此线程安全问题中,一开始将waitQueue设计为LinkedList来实现根据优先级重新排列,但是LinkedList并非线程安全类,因此在对waitQueue进行操作时,对waitQueue对象进行上锁,使用同步块的方式来保证线程安全。
  • 3、为了避免轮询问题,电梯线程当等待队列中没有乘客,同时电梯内也没有乘客时,电梯线程会调用wait()(以等待队列为对象设置锁),进入等待状态,直到调度器将新的乘客放入等待队列中,同时调用notify()(依旧以等待队列为对象设置锁),唤醒电梯线程,从而避免电梯线程的轮询问题。

运行策略

总共实现了两种运行策略:LOOK、 ALS;

  • LOOK策略:总结来说,LOOK策略是以电梯运行方向为重点指导;当电梯在等待时,根据第一个进入等待队列的乘客的等待楼层,来决定电梯的运行方向;此后当电梯运行过程中,会将途中同方向乘客进行捎带,直到将此方向的乘客全部送达完毕后,再根据新乘客决定新的运行方向。
  • ALS策略:ALS策略是以乘客优先级为重点指导。当电梯内无人时,电梯根据所有乘客中优先级最高的乘客的等待楼层设定电梯运行方向,若有同方向可以进行捎带;当电梯内有人时,以电梯内最高优先级的乘客的目标楼层设定电梯运行方向。
  • LOOK策略在乘客较多的时候表现较好,可以使运行时间更短;ALS策略在乘客较少的时候表现较好,可以更好地实现电梯根据优先级的调度的要求。
  • 设计strategy接口,LOOK以及ALS分别实现strategy接口,在电梯运行时可以更灵活地选择不同的运行策略。

其它设计

  • 1、设计Person类,用来记录乘客的各种信息。
  • 2、设计Queues类,用来管理waitQueue以及exitQueue等队列。(很可惜,直到第二次作业才发现Queues类也可以设计成一个线程安全类,此次作业并未设计,导致电梯线程中代码较为冗余)
  • 3、设计枚举类DIRECTION、ADVICE,分别用来记录电梯的运行方向以及电梯的运行策略。
  • 4、设计Route类,来管理电梯停靠楼层等信息。

3、UML图

img

4、协作图

img

从协作图可以看出不同线程之间的创建顺序以及关系,但其中Route和Queues并非线程,而是在这两个类对部分方法进行了上锁,来实现线程安全,故体现在了协作图上。

5、bug总结

本次暂无bug。

二、第二次作业

1、作业要求

相比第一次作业,移除了乘客指定电梯的要求,同时增加了电梯临时调度的请求。

  • 移除指定电梯则需要调度器来实现电梯指派功能,因此需要实现分配策略来达到性能优化。
  • 临时调度是一种新的电梯行为,同时在调度时电梯不能接受任何乘客请求。若将临时调度也同乘客请求当作一种请求,个人认为临时调度的优先级将高于任何乘客请求,电梯需要优先处理临时调度请求,同时在处理完临时调度请求前无法处理任何其它乘客请求。

2、架构分析

  • 1、为了避免临时调度请求被过久的等待,在requestTable中新增了一个临时调度请求队列,同时新增临时调度请求的处理线程,与原先的单一指派乘客的调度器分离,一来可以解决临时调度请求前乘客请求较多而导致临时调度请求延误的问题,二是可以避免单一调度器的方法过多,违反了单一职责原则。
  • 2、 同时电梯线程中也需要相对根据临时调度请求作出改变。首先是电梯和临时调度分配器间需设立共享对象,同时需要解决互斥问题;其次,电梯线程中也需要拔高临时调度请求的优先级,需要在处理完临时调度后,才可以接受任何乘客请求。看似这其中存在了一个同步问题,但是由于处理临时调度以及接受乘客都是在同一线程中处理的,所以实际上并不存在线程与线程之间的同步问题。但是很可惜,当时写代码的时候,由于接受乘客的功能与调度器耦合性较高,导致无法将其分离,而自己造就了一个同步问题,从而引起来性能上的损失。
  • 3、 对于调度器中的分配策略,主要完成了两种策略:均分策略和影子电梯
    • 均分策略:相对容易实现,六个电梯依此分配。
    • 影子电梯:每新来一个乘客,则对六个电梯此时的状态进行复制,并通过模拟电梯运行的方式,通过加入此乘客,来计算最终运行时间,选择运行时间最短的电梯。
      • 影子电梯带来的新问题就是引入了新的共享对象,即电梯的状态。同时引出这是同步问题还是互斥问题的思考:应该是互斥问题。
      • 1、身为调度器,其不应该会阻碍电梯的运行,尽管调度器中仍然在模拟计算,电梯不应该等待每次调度器模拟结束后再运行,这甚至是不符合常理的。
      • 2、但是不设置为同步会引起模拟过程中电梯的状态不准确,比如在模拟过程中,有一个电梯接收到临时调度请求,但是模拟中并未考虑临时调度,导致有可能将乘客分配给一个实际运行时间较长的电梯。
      • 综合考量下来,1的影响将显著高于2,1在每次有新乘客时都会引起性能损失,而2只在有临时调度进来时,才有可能引起性能损失,这个损失的量将远远小于1.

3、UML图

img

  • 相比第一次作业,增加了电梯的行为:解决Sche行为,强制乘客退出电梯行为(force)以及尝试强制乘客退出(在调度时候使用,若乘客目标楼层方向与调度方向不一致则使乘客离开电梯)。
  • 电梯中增加了取消receive的行为,主要是通过重新将乘客push回requestTable实现。
  • 在shadowStrategy中使用shadowElevator, shadowElevator首先对目标电梯的属性以及状态进行深拷贝,再通过模拟得到最终运行时间。但是,仅仅是属性深拷贝,电梯运行的行为是各自写的,导致原电梯方法的改变难以即使同步到shadowElevator中,会导致bug出现。究其原因是一开始对电梯以及电梯线程无法区分开,而影子电梯又仅仅只是一个电梯。若可以将原电梯线程中的电梯抽离开,则影子电梯可以直接调用电梯的方法,从而来避免了上述问题。

4、协作图

img

  • 相比第一次作业,增加了临时调度请求的处理线程,同时调度器与临时调度请求线程间共享requestTable对象。

5、bug总结

本次bug有:

  • 1、在修改了普通电梯遍历中删除的bug后未同步到影子电梯中,导致影子电梯出现bug。
  • 2、在进行处理临时调度行为中,尽管写了forceOut方法,结果最终漏写了这一方法,导致没有强制所有乘客退出电梯。这其实与正确性检测有极大的关系。

第三次作业

1、作业要求

在第二次作业的基础上,新增update请求,收到请求的电梯强制退出所有乘客,原地进行改造。改造后形成双轿厢电梯(两电梯共用电梯井,且拥有一个公共楼层,因此需注意电梯碰撞问题)。

2、架构分析

  • 与第二次作业相同,对于update的请求,也是新增一个Updater线程,将update分别发送给不同电梯线程。

  • 电梯解决update请求的行为几乎与第二次作业的sche一样,唯一有区别的是update Begin和End的输出:Begin需要两个电梯将所有乘客强制退出的时候输出,End需要两个电梯都完成开关门的要求后输出。因此设计了一个UpdateProcessor线程来接受两部电梯的信息,当一个电梯准备好begin或end的时候,会向UpdateProcessor更新自己的状态,当UpdateProcessor判断现在可以进行begin或end,再向两个电梯发送新的信号。同时在UpdateProcessor输出update begin和end。

  • 第二个需要解决的问题是双轿厢电梯有可能相撞的问题。同上,设计了电梯井Well的线程来处理此方法。电梯若要进入共享楼层,则像此线程发送请求,若Well判断公共楼层有电梯占用,则向此电梯发送leaveSignal信号,当电梯离开,再向原请求电梯发送信号,让其进入。

    • 实现方法: 每个双轿厢电梯对应一个电梯井,电梯井为中间楼层设置一个ReentrantLock,同时设置两条件WaitCondition以及OccupyCondition,来解决两电梯(发送请求-等待-Well发送leaveSignal-电梯离开公共楼层-Well notify原电梯-原电梯进入公共楼层)的同步问题。

3、UML图

img

  • 相比第二次作业,由于电梯的行为越来越多,因此很有必要将电梯与电梯线程进行分离。电梯管理这自身的信息状态,以及运动的各种方法;而电梯线程负责接受各种request,接受strategy给予的advice来调度电梯,处理sche以及update。因此进行了一番大重构。
  • 同时,由于电梯与线程的分离,使得双轿厢可以单独做成新的类,继承电梯类,只需要在进入或离开公共楼层的行为进行重构,在这两个方法中向Well类发送信号等待,接受唤醒并得以进入公共楼层。同时使Update进行改造的时候,只需要将电梯线程原来管辖的Elevator变成双轿厢电梯即可。

4、协作图

img

5、bug总结

  • 因为进行了大重构,导致出现了第二次作业的同样的遍历删除类bug
  • 在Well类负责处理双轿厢公共楼层冲突时,出现了两电梯同时申请进入公共楼层而同时允许进入的情况,导致出现了电梯相撞问题。解决关键在于枷锁区域,一定要在先发送请求的电梯正式进入共享楼层后再处理另一电梯。

总结

同步块与锁

  • 一开始是将共享对象requestTable的所有读和写方法都加synchronized上锁,来实现一个线程安全类。后续由于线程越来越多,共享对象也更多,部分对象无法做到单独成一个线程安全类,因此选取了更加灵活的ReentrantLock以及Condition进行加锁,这样子一个线程可以提供上锁以及解锁的方法来更加灵活实现同步块的设置。
  • 锁与同步块:一把锁只能被一个线程拥有,其它线程则位于锁的等待队列中,直至锁被释放。得到锁的线程可以进入同步块中代码,此时同步块中语句可以视为原子的,不会有其它线程进入此语句中。

调度器分析

  • 为person、sche与update各自设计了一个调度器,inputThread与三个调度器共享requestTable对象,形成了一生产者-多消费者模式,有利于更及时地处理sche以及update请求。
  • 三个调度器分别与电梯共享电梯中的等待队列、sche队列以及update,从而进行交互。
  • 调度策略中分为两种:
    • 分配策略主要实现了均分策略以及影子电梯策略,通过多次对同一输入数据对比发现影子电梯效果显著的好。
    • 运行策略主要实现了LOOK以及ALS,LOOK策略在乘客较多的时候效果显著,ALS策略在乘客较少的时候效果显著。
  • 关于加权等待时间:主要通过将等待队列,每次同楼层有新乘客进入,根据优先级对乘客进行重新排队。电梯接受此楼层时,则从队首开始捎带乘客。
  • 关于电量:主要通过避免无用电梯行为来解决。比如电梯运行中,临时改变运行方向,导致非必要地多耗了运行于楼层间的电量。

稳定/易变分析

  • 稳定:
    • 1、多线程交互模式:
      • 生产者-消费者模式保持稳定: 调度器分配请求时都是使用生产者-消费者模式。
      • 处理update时使用了观察者模式,电梯每次有关update的状态更新,都会汇报给观察者,由观察者判断下一步行为
      • 单例模式,对所有乘客进行统一管理,通过判断乘客是否全到达目标楼层来判断乘客调度器是否结束。
    • 2、电梯运行策略保持稳定:Look以及Als在第一次作业形成后,后两次作业无需更改。
    • 3、队列类保持稳定。
  • 易变:
    • 1、调度器分配策略易变:
      • 均分策略中,需要考虑第三次作业引入的双轿厢电梯,当乘客的等待楼层该电梯无法送达时,则需要跳过此电梯。
      • 影子电梯中:也需要考虑双轿厢电梯,在计算时间的时候,需要多考虑双轿厢电梯之间换乘的时间。
    • 2、楼层类会改变,也是因为双轿厢电梯的引入。
    • 3、电梯的行为也需要改变,主要由于新增的sche、update以及进入公共楼层等行为需要不断添加新方法。

debug方法

与室友进行合作,一个室友负责制作有策略的数据生成器,涵盖同一时间多请求、多大幅度跨楼层请求、多sche、多update等策略。
我负责写正确性检测,主要通过输入记录所有请求,根据输出的结果一步步更改每个电梯以及请求的状态,在此过程中判断是否有违反题目正确性的内容;同时在输出结束后,对所有请求判断是否正确完成。
还有一位室友自己根据输出设计了一个可视化的debug工具,我们可以直观看见电梯运行、乘客的状态。
最后为了检验正确性,结合AI写了一个多线程十分简陋的评测机,主要结合室友的数据生成器指定输入,然后通过多线程一次性跑50次数据,同时限定运行时间,最后结合自己写的检查正确性来debug。(可惜自己写的内容也有一个正确性漏检测,刚刚好把自己第二次作业hack了...)。

心得体会

  • 首先最深的体会就是引入了多线程的概念,之前对程序的理解都是在单线程的环境下,而多线程的引入带来了许多优点:如最大化利用CPU资源、提高程序的响应速度等等。
  • 其次是对于锁的理解,一开始只会对一个共享对象的所有方法上锁,甚至在单线程里也莫名其妙上了个锁,降低了性能。随后在实践中,对同步互斥有了更深地了解,也对加锁的时机,wait()以及notify()、reentrantLock以及Condition的使用有了更深入的理解:
    • 上锁主要保护的是读与写或者同时写问题;wait与notify主要解决的是线程间的同步问题。通过上锁实现线程安全;同时也可以设计线程安全类来实现线程安全,从而避免大量加锁引起代码过于复杂。
  • 最后从层次化角度来看:
    • 主要是要明确多个线程的作用以及功能,其次明确不同线程之间如何交互。
    • 然后为了避免轮询问题,也需要使用wait方法来停止不必要的空转,这时候更需要把握何时通过其它线程来唤醒此线程。
    • 最后在线程的启动以及结束时机也需要把握好,基本思路是从顶向下式的启动线程,最后从底向上式的结束线程。
  • 最后的最后,多线程单元终于结束,虽然已经遍体鳞伤,但是确实受益匪浅。
...全文
22 回复 打赏 收藏 转发到动态 举报
写回复
用AI写文章
回复
切换为时间正序
请发表友善的回复…
发表回复

269

社区成员

发帖
与我相关
我的任务
社区描述
2025年北航面向对象设计与构造
学习 高校
社区管理员
  • Alkaid_Zhong
加入社区
  • 近7日
  • 近30日
  • 至今
社区公告
暂无公告

试试用AI创作助手写篇文章吧