OO第二单元总结

一、三次作业中同步块的设置和锁的选择

由于每次作业均为上一次作业的增量开发,故此处只分析第三次作业。

代码中:

  • Count类(用于记录电梯系统里现有人数)

  • Distributor类(用于分配人员至相应楼座或楼层)

  • Mask类(记录各层横向电梯的掩码)

  • Output类(安全输出)

  • Queue类(等待队列)

    为共享对象,同步块的设置采取了"给类中每个方法加上synchronized"的方法,即锁为this对象的锁。这样的设置也保证了任何一个时刻最多只有一个线程会访问Person类的某个对象,故Person类无需上锁。

    优点:实现方式简单

    缺点:给非静态方法上锁,实际上也就是给这个类的某个对象上锁,导致任何一个时刻只有一个线程可以访问该对象,效率较低;如果给静态方法上锁,那么将会导致任何一个时刻只有一个线程可以访问该类的所有对象中的一个,效率会非常低。

    可能的改进措施:给类中的属性上锁

二、三次作业中的调度器设计

作业中采用了输入、电梯为线程,调度器为共享对象类的设计方式,同一楼层或同一楼座仅设置一个等待队列,电梯采用自由竞争的方式"抢人",故调度器仅需要完成

  • "将输入线程中的乘客请求送至相应楼层或楼座的等待队列"

  • "将换乘的乘客送入下一阶段的楼层或楼座等待队列"

  • "处理输入线程中的新增电梯请求,创建新的电梯线程"

    这三个功能。(准确来说,这个调度器更像是一个对输入线程中的请求做出响应的类,亦称分配器+电梯生成器

调度器与输入线程和电梯线程进行交互,其中第一个和第三个功能是与输入线程进行交互,第二个功能是与电梯线程进行交互。

三、线程协同的架构模式

1、第三次作业设计思路

  • 每个楼层和每个楼座均有可能有多部电梯,每部电梯采取LOOK算法接人

  • 每一楼层或每一楼座只有一个等待队列,同一楼层或同一楼座多部电梯之间自由竞争去"抢人"

  • 读取到一个新来乘客时,便根据楼层掩码计算出该乘客的换乘楼层,将该乘客的轨迹分为三段(静态拆分,有些段可以在初始时即完成),并在Person类中设置一个3位二进制掩码用于标记这三段轨迹的完成情况

  • 每当乘客从某个电梯出来时,判断该乘客三段轨迹是否均已完成,并决定是否将该乘客送入调度器

  • 设置Count类,其中记录了输入线程结束的标志变量以及电梯系统内当前人数的变量,当且仅当前者为true且后者为0时,使用distributor.setEnd(),distributor会向每个等待队列发出结束信号。当某个电梯中没有乘客、该电梯对应的等待队列为空且结束标志位为true时,该电梯线程可以结束。

2、第三次作业类图

(1)迭代开发:

第一次作业含有上图中所有白色框的类,蓝色框的InputThread类和BuildingElevatorThread类,而粉色框的Output类;

第二次作业增加了横向电梯,故增加了蓝色框的与BuildingElevatorThread类类似的FloorElevatorThread类;

第三次作业增加了换乘和横向电梯仅在特定位置开门的掩码,故增加了粉色的Count类和Mask类

由于第一次架构设计得当,故在第二、三次作业的迭代开发中较为轻松,且第三次作业的未来扩展能力较强。

(2)模块设计:

蓝色框的类为线程,粉色框的类为单例模式(实际上Distributor也应该设计成单例模式,但是第三次作业懒得改了×

3、第三次作业UML协作图

4、第三次作业指标度量分析

(1)总代码规模

(2)类复杂度和方法复杂度
  • 类复杂度

 

其中三个线程类复杂度较高,原因是线程运行逻辑较复杂,尤其是电梯线程中还有多个模拟电梯行为的方法。

  • 方法复杂度

(仅截取标红部分)

其中横向、纵向电梯的checkUpDown()、doorOpenClose()、run()方法复杂度较高,Person类中寻找换乘楼层的方法复杂度较高,Queue类中寻找横向、纵向电梯下一运行方向的方法复杂度较高。

四、三次作业中出现的bug

1、第一次作业

第一次作业最初采用LOOK和ALS混合算法,导致思路很乱,主请求傻傻搞不清,出现了挺多bug。在第二次作业开始前重新修改了单部电梯的接人算法,改为了纯粹的LOOK算法(啊,终于清爽了。

  • "吃人"电梯:在checkUpDown()中判断是否有捎带时,把可以捎带的对象从waitingQueue中remove掉了!导致doorOpenClose()中发现人不见了……

  • 门外门内乘客观看电梯鬼畜开关门:checkUpDown()判断是否有上下电梯需求时,对上电梯的判断未加入passenger.size() < capacity的判断,导致有捎带需求但电梯满员时,也会判断为需要开关门,但在doorOpenClose()中却上不去人,最终造成一直开关门

  • 懒汉电梯:Queue中有方法未加notifyAll,可能导致无法唤醒其他线程

  • 从不顺路接人的电梯/上天入地电梯——强测

  • 输出线程不安全导致时间戳非递增——互测

  • 下完乘客后若电梯里没人,且电梯需要改变运行方向,这个电梯好像会先关门再开门接人……(这个不算bug,但是会降低性能

2、第二次作业

第二次作业非常顺利,没有出现bug(好耶!没有人在这个不成熟的电梯系统中受伤!

3、第三次作业

  • 不主动上报开关门信息电梯:新增横向电梯时忘记将掩码加入Mask类中

  • 白干活电梯:横纵向电梯人出门后在放入distributor之前忘记使用setNextPlan更新Person的当前出发地和当前目的地

  • 送人不送到位电梯:结束标志不能仅仅依据输入线程读到NULL,还要依据电梯系统里是否还有人

  • 乱开关门电梯:checkUpDown()中首先需要判断横向电梯的可开关门信息

  • 迟迟不肯下班电梯:Count类里的subCnt()和setInputThreadEnd()都要判断此时是否可以进行distributor.setEnd();,否则可能导致电梯线程无法结束

  • 贼船电梯:采用自由竞争的方式,但电梯在"抢人"的时候,没有结合掩码判断能不能接某个人,导致横向电梯可能接了某个无法通过该电梯到达目的地的人,使得该电梯不断在楼层内循环到达A,B,C,D,E座(这个bug仅可能在某层楼有多个电梯时出现)——强测

总:上述所有bug中有三个强测发现的bug,一个互测发现的bug

五、发现别人bug所采用的策略

1、容易出现问题的地方:

  • 线程安全问题(如共享对象加锁、输出时间戳递增、notifyAll)

  • 轮询问题

  • 是否有对满载的判断和处理

  • 如果横向电梯类是由纵向电梯类的代码复制过来的,很有可能有的细节没有完全修改好

  • 是否充分考虑到了掩码带来的影响(电梯只能在特定位置开关门、横向电梯接人时需要考虑自己能不能将这个人送到目的地等)

2、测试策略:

总策略:阅读代码+构造有易错点的数据

发现线程安全相关的问题主要采用阅读相关代码的方法。

3、本单元的测试策略与第一单元测试策略的差异之处

本单元相同的输入数据,多线程电梯多次运行的结果基本是不一样的,线程安全问题可能需要运行多次才能复现;而第一单元同一个输入数据,同一个程序每次运行的结果都是一样的。

本单元程序的bug有可能与各个请求输入的时间有关,而第一单元bug与输入数据的时间无关。

六、心得体会

1、线程安全

第一次接触多线程,感到好奇的同时也感到有些困惑。逐渐揭开多线程的面纱,死锁、轮询、同步块、锁等等都是线程安全问题中所需要考虑的。使用wait()可以有效避免轮询,对于多个线程可能同时访问的共享对象需要加锁……弄清程序中的线程有哪些,共享对象有哪些,它们之间是如何交互的是非常重要的,且弄清了这些基本情况有利于我们更好地保证线程安全。

目前接触到的多线程知识还非常有限,以后可能多阅读相关的书籍来进一步了解多线程,毕竟多线程的用途是非常广泛的。

2、层次化设计

在第一单元时,初步接触面向对象思维和层次化设计,当时是感到有些吃力的。而在第二单元的三次作业中,对架构的整体把握明显有所加强,能够更加清晰地知道自己设计的类需要完成哪些功能。在每次写代码前,我都会先把该次作业的需求整理好,用更加简洁的语言描述一遍,之后会设计好可能需要的类以及这个类可能需要的属性和方法。在草稿纸上设计架构的时候很明显地能够看到自己的思路存在哪些问题,有效地避免了急于编码导致出现很多bug且不知道问题在哪的情况。

第一次作业:

 

第二次作业:

 

第三次作业:

 

希望在以后的课程中能够继续贯彻面向对象思维和层次化设计,努力提高自己的架构设计水平和编程水平。

 

 

posted on 2022-04-29 14:46  lr20  阅读(45)  评论(1编辑  收藏  举报