OO_Unit2_Summary

OO第二单元多线程总结

一、三次作业设计策略

个人三次作业使用的都是synchronized语句块,都是生产者-消费者模型,都是电梯自调度系统。

  • 第一次作业策略
    第一次作业个人使用生产者-消费者模型。作业里一共有三个线程类,分别为Main线程、InputThread线程与Elevator线程。其中Main线程创建另外两线程,InputThread线程作为生产者,Elevator线程作为消费者。另外有一个Control类作为二者的缓冲(调度器),用来存储生产者生产的请求。其中个人设置的电梯为自调度电梯,会自行去control类中寻找请求并使用捎带策略。

  • 第二次作业策略
    第二次作业在第一次基础上增加了多部电梯。第二次作业在第一次作业的迭代后并没有太多改动,修改较大的是电梯的自调度算法。因此第二次与第一次作业在设计策略上一致。

  • 第三次作业策略
    第三次作业会动态申请不同类型电梯且需要换乘。如果继续沿用第一次的结构会使Control调度器极度臃肿。因此个人在第三次作业进行了代码的重构。
    第三次作业个人设计了四个线程类,分别为Main线程、InputThread线程、ControlThread线程和ElevatorThread线程。同时还有两个调度类FirstSchedule与SecondSchedule。其中FirstSchedule类作为InputThread与ControlThread的缓冲,SecondSchedule作为ControlThread与ElevatorThread的缓冲。中间的ControlThread线程作为换乘的调度中心去分配乘客请求至不同的电梯。具体信息在之后的第三部分。
    但是第三次作业最后产生了暴力轮询的问题,因此多个测试点程序CTLE,未通过中测。

二、 第三次作业设计架构的可扩展性

关于设计架构的可扩展性,个人以SOLID的几个指标为主进行衡量。

  • SRP 每个类或方法都有一个明确的职责
    在第三次作业中,个人设计的几个类基本都有明确职责,InputThread线程读取输入转换为请求并放置于FirstSchedule中,ControlThread线程从FirstSchedule中取出请求并根据请求创建电梯或是分配请求至SecondSchedule中的不同电梯队列。之后ElevatorThread线程从SecondSchedule中取出请求并送至目的地。
    但问题在于我的ElevatorThread做的事情太多了,做了很多调度器应该做的事情。并且类中的很多方法有重复的地方或是职责不明确的地方。

  • OCP 无需修改已有实现(close),而是通过扩展来增加新功能(open)
    个人的第三次作业中,有些类可实现OCP原则,但还有几个类比较难实现OCP原则。个人的ElevatorThread线程就难以使用继承的方法去扩写,与之联系紧密的SecondSchedule也有一定困难,其他类需根据具体情况而定。

  • LSP 任何父类出现的地方都可以使用子类来代替
    个人第三次作业除线程类继承Thread外,并无使用继承机制,因此LSP原则不适用于本次作业。但如果之后的扩展使用了继承机制,那么,个人的代码大可能是无法满足LSP原则的。

  • ISP 通过接口来建立行为抽象层次具有更好的灵活性
    个人第三次作业中并没有使用过多接口,因此不满足ISP原则。但个人认为像电梯上下行等方法可以使用接口实现,这点以后可以改进。

  • DIP 依赖倒置原则
    个人第三次作业并没有满足DIP原则,个人缺乏此方面的意识,因此代码重构时未考虑到此问题。

  • 其他原则
    个人代码可以基本满足懂我原则、显示表达式原则与信任原则,但一定程度上违背了均衡原则(ElevatorThread做了过多事情)、局部化原则与完整性原则。

三、 基于量度分析程序结构

第一次作业量度分析

第一次作业类与方法一定信息如下:
OO_Unit2_Summary_第1张图片
OO_Unit2_Summary_第2张图片
OO_Unit2_Summary_第3张图片
第一次作业UML类图:
OO_Unit2_Summary_第4张图片

本次作业共设计了5个类,Main、Person、InputThread、Control、Elevator。其中Main为主线程创建InputThread线程、Elevator线程以及Control实例,Person类保存请求信息(第一次作业未公开PersonRequest的构造类,因此使用Person类,之后作业仍使用Person类),InputThread线程处理输入并增加请求至Control实例,Elevator线程从Control实例中提取请求。
由于本次作业的难度不高,因此UML类图比较清晰,可以看出生产者-消费者模型的轮廓。由UML可知,本次作业的重类是Elevator类与Control类,这两个类在代码中发挥关键作用。同时,本次作业代码长度,方法数量,以及长方法大部分集中与Elevator类,Elevator承担了过多的责任,可以再适当地分配一定任务给Control类。并且,本次作业在扩展性上还有一定问题,扩展电梯没有问题,但动态设置电梯、换乘、乘客优先级等情况就有一定麻烦。

Main线程协作图
OO_Unit2_Summary_第5张图片
InputThread线程协作图
OO_Unit2_Summary_第6张图片
Elevator线程协作图
OO_Unit2_Summary_第7张图片
线程协作大体与上相同,Main为主线程创建InputThread线程、Elevator线程以及Control实例,Person类保存请求信息(第一次作业未公开PersonRequest的构造类,因此使用Person类,之后作业仍使用Person类),InputThread线程处理输入并增加请求至Control实例,Elevator线程从Control实例中提取请求。

第二次作业

第二次作业类与方法一定信息如下:
OO_Unit2_Summary_第8张图片
OO_Unit2_Summary_第9张图片
OO_Unit2_Summary_第10张图片
第二次作业UML类图:
OO_Unit2_Summary_第11张图片

第二次作业是第一次作业的迭代,因此在各方面与第一次基本无异。其中协作图与第一次作业完全一致,因此未将其展示。
第二次作业与第一次相比,添加了多部电梯,因此个人改变了电梯的自调度算法,让它们可以像单部电梯时一样运行。因此第二次作业迭代时的改动部分只有Main线程中创建电梯的部分、电梯的自调度算法以及调度器的一些功能。其中各个类的方法个数基本不变,但内容有一定改变。
根据UML类图,第二次作业与第一次作业的优缺点大致相同。

第三次作业

第三次作业类与方法一定信息如下:
OO_Unit2_Summary_第12张图片
OO_Unit2_Summary_第13张图片
OO_Unit2_Summary_第14张图片
OO_Unit2_Summary_第15张图片
OO_Unit2_Summary_第16张图片
第三次作业UML类图:
OO_Unit2_Summary_第17张图片
OO_Unit2_Summary_第18张图片

第三次作业个人进行了重构。重构后一共有9个类,分别为Main、Person、Elevator、SafeOutput、InputThread、FirstSchedule、ControlThread、SecondSchedule、ElevatorThread。其中Main为主线程,负责InputThread线程、ControlThread线程、FirstSchedule实例、SecondSchedule实例的创建。Person存储乘客请求,Elevator存储电梯请求,SafeOutput为线程安全的输出。InputThread线程作为生产者,ControlThread线程进行调度,ElevatorThread线程为消费者。
根据以上信息得,本次重构后代码复杂度大幅上升,类数量与方法数量以及方法长度也与前两次作业不是一个量级。之后在代码编写时还是应该多注意代码的编写问题,在适当位置可以引入一系列接口完成特定任务。
根据UML类图,本次作业的结构还是比较清晰的,生产者与消费者模型的轮廓仍然可见。本次作业的缺点也有有很多。本次作业的重点类在于两个线程类与一个调度器。ControlThread作为本次作业的调度器负责着乘客的换乘,而个人解决乘客换乘问题使用的是打表法,因此ControlThread的复杂度升高。而ElevatorThread是自调度电梯线程,以及SecondSchedule作为电梯的缓冲队列,二者相当于前两次作业的Elevator与Control,因此二者的复杂度同样偏高。如果可以的话,还是应该使用一定的接口去帮助这些重类去解决问题。

Main线程协作图:
OO_Unit2_Summary_第19张图片
InputThread线程协作图:
OO_Unit2_Summary_第20张图片
ControlThread线程协作图:
OO_Unit2_Summary_第21张图片
ElevatorThread线程协作图:
OO_Unit2_Summary_第22张图片

本次作业协作流程大致:Main线程创建InputThread线程、ControlThread线程、FirstSchedule实例、SecondSchedule实例。InputThread线程读取输入转换为电梯请求或是乘客请求并放置于FirstSchedule中,ControlThread线程从FirstSchedule中取出请求并根据请求创建电梯或是分配请求至SecondSchedule中的不同电梯队列。之后ElevatorThread线程从SecondSchedule中取出请求并送至目的地。

四、 bug分析

本单元个人前两次作业无bug,第三次为无效作业(公测结果为CTLE,代码产生了暴力轮询)。对于第三次作业的bug分析如下:
个人的代码在每次判断队列为空后都会加上wait,但是最后仍旧是CTLE。原因我认为有多个。一是我个人在写代码时盲目notifyall,因此线程被多次唤醒;二是个人在判断队列是否为空时,使用了非常复杂的判定条件,甚至还需要去获得其他电梯的状态;三是电梯的关闭,我个人的关闭条件也太过复杂。可以设置一个全局的储存乘客请求的容器,存储所有的乘客请求,完成一个乘客请求后将其丢出容器。最后可以根据容器剩余以及输入是否结束判断是否应该结束电梯线程;四是个人将太多信息储存于SecondSchedule类中,应该将SecondSchedule拆分为多个小类,每个小类储存关系比较密切的一类数据。

五、 寻找他人代码的bug

个人为了寻找他人bug而采取的策略有两个,一是手动构造复杂数据,依靠数据的复杂程度去hack他人;二是查看他人代码,去发现他人bug。以上两个方法的有效性并不高,因此本单元我只成功hack他人一次。
对于发现他人线程安全问题的策略,个人会去查看他人代码,以此为基础去发现他人的线程安全问题,并在本地多次测试(但因多线程难以复现的特性,效率低)。
第二单元的策略与第一单元极为不同。用不恰当的方式来说,第一单元的bug是“确定”的,只要我发现了,我就一定可以hack他人。但是第二单元不同,有时即使你找出了他人的bug,但是你无法复现,且互测对数据有着较为严格的管控,因此hack他人更为困难。

六、 心得体会

  • 线程安全方面
    线程安全非常重要,在使用多线程时,一定要构造好线程安全类,保障共享对象的数据安全。
    同时也不能过分保护,把对象的所有方法都无脑上锁,这会影响程序的性能与效率。
    并且上锁后不能无脑notifyall,这同样会影响效率,使得不该苏醒的线程苏醒。
    避免使用notify,notify可能会导致某个线程无法苏醒。
    个人还是应该多去学习除synchronized以外的其他语句,比如lock与unlock的使用等等。

  • 设计原则方面
    在代码书写前应该构思好代码的大致结构、使用的代码模型,考虑好以后的迭代方式,为以后的扩展打下基础。
    构思时要多注意SOLID等原则,保障代码的扩展性。同时还要注意懂我原则、显示表达式原则、信任原则等等的应用,使代码有较高的可读性。

六、 结束

以上便是我第二单元的总结。

你可能感兴趣的:(OO_Unit2_Summary)