面向对象程序设计第二单元总结
面向对象第二单元总结
一、前言
本单元的作业以Java多线程设计为核心,通过电梯调度的实例模型,涵盖了生产者—消费者模式、单例模式、多个线程的交互、线程安全维护等方面的知识。
二、历次作业架构分析
(1)第一次作业
本次作业的基本目标是模拟单部多线程电梯的运行。
1. 架构设计
-
类的组织:
本次作业由于仅有一部电梯,因此我选择了较为简单的生产者—消费者模式,并未添加调度器类。在电梯运行时,仅有两个线程:
Elevator与InputThread。 -
线程同步与锁:
经分析,本次两个线程可能产生交互的变量是存放所有新来人员请求的类
RequestList,我将这个类当作一个静态类来处理,使得两个线程更容易访问和修改ArrayList。
InputThread为生产者,每当读取一个有效输入时,就获取RequestList的锁并将请求加入队列,并在合适的时机唤醒电梯。
Elevator为消费者,在电梯调度算法运行时,访问当前等候的请求队列需要获取其锁;若存在可接的请求即进一步运行,否则就进入wait状态,等待输入线程的唤醒。 当
InputThread结束,而且RequestList中不再有请求且电梯已经送走了内部的所有请求,便可以结束程序。 -
电梯调度算法:
在网上查询了相关资料后,从ALS、SCAN、LOOK等算法中选择了LOOK算法,并按照自己的理解实现了相应的逻辑。(其中,蓝色部分表示需要获取
RequestList的锁)![]()
Random与Night模式下,我均选择了LOOK算法实现;Morning模式下,我选择电梯等待载满6人再出发一趟。 -
值得一提:
参考往届作业,电梯的一些静态属性可能因电梯种类不同而不同,为进一步深化电梯类的构造,我将大部分参数作为成员变量(
id,moveSpeed,openSpeed,closeSpeed,capacity等),以通过工厂类来获取对应需求的电梯,虽然有些直到最后一次作业也没用上,但这样可能更加符合面向对象的思想,增加了可扩展性。
2. 性能分析
-
代码长度
实际行数335行,主要复杂度集中在
Elevator类中 -
方法复杂度
平均较低,主要复杂度集中在判断电梯转向函数
judgeDir()和Morning类型的调度算法。(前者需要同时循环判断候乘列表和电梯内列表)![]()
-
类图
![]()
在画图的时候发现蓝色箭头指向竟能直接改变类的继承关系,回过神来代码一片混乱在
Test类中初始化了TimableOutput,并实例化线程和调用了线程的run方法。 -
性能
本次作业中,
Random模式和Night模式均能较好适配LOOK算法,性能分较高;美中不足的是,我的Morning策略采用的是等满6个人再运行的策略,在等待的过程中可能会耗费一些时间,而且并没有进行远近的排序。 总体而言,本次作业我对架构和性能都比较满意。
3. Bug分析
本次作业中强测和互测均未出现Bug,但在自己完成作业的过程中,有一些值得注意的地方:
-
LOOK算法的逻辑:我采用boolean变量
direction表示电梯当前方向,在逻辑判断上:(req.getToFloor() > req.getFromFloor()) == direction只能涵盖一部分同向请求的问题,需要进一步细化判断。
-
RequestList静态变量的使用在获取锁时,应采用
synchronized (RequestList.class)的形式,并完成一系列对静态变量操作的方法。
4. 测试策略
模仿讨论区同学分享的用C语言编程模拟定时输入的操作,并自己写代码随机生成20到40组输入请求。判断是否存在死锁、循环、waiting、电梯逻辑错误等。
(2)第二次作业
本次作业要求模拟多部同型号电梯的运行,并要求能够响应输入数据的请求,动态增加电梯。
1. 架构设计
-
类的组织:
本次作业使用多部电梯进行运送,同时可以动态增加电梯,于是我选择增加调度器
Dispatcher来更规范、方便的管理调度,防止多电梯直接竞争。 -
线程同步与锁:
由于增加了电梯线程和调度器线程,本次作业线程的交互更加复杂,于是我按照如下的策略进行调度:(蓝色代表
ArrayList结构,黄色代表线程)![]()
由此形成由上至下的调度结构,只有相邻两级之间存在共享变量同步问题,这类同步问题解决方案与第一次作业类似,除了获取锁的对象外,无需过多修改。
-
调度算法:
本次我没有在第一次的基础上过多优化;电梯内部仍旧采用LOOK算法运行,只是在调度器内如下安排:
类型 策略 Morning Type等满6人便交给一个电梯,再满则交给下一个,以此类推; Random & Night Type采用 Random类随机生成一个1~电梯总数目的数字index,并将请求交给处于index位的那一台电梯;
2. 性能分析
-
代码长度
实际行数469行,在
Eleator和Dispatcher的不同策略中,产生了重复代码(明明LOOK就可以满足电梯运行,但我忘记删去第一次作业电梯的Morning方法,和调度器中的Morning方法重复) -
方法复杂度
良好,除去第一次作业中同样复杂度较高的两个方法,并未增加更复杂的地方
![]()
-
类图
![]()
细节类似于前述架构设计中的图
-
性能:
本次作业总体性能分较好,依旧同上次作业,在
Random和Night模式下表现良好,但再Morning模式下耗时较长,可能测试数据在单纯等待载满的情况下并不友好,导致两个点仅有80分,想知道其他同学是如何处理Morning的。(貌似不搞特殊化反而更好?)
3. Bug分析
本次作业强测和互测均为出现Bug,在本地自测时,发现有一处在第一次作业基础上修改锁对象时忽略了(第一次作业电梯直接获取RequestList主队列的锁,但第二次修改为电梯获取自己的等待队列的锁),导致出现无法正常结束的Bug,修复后才较为完善。
4. 测试策略
同第一次作业
(3)第三次作业
本次作业要求模拟多部不同型号电梯的运行。型号不同,指的是开关门速度,移动速度,限载人数,以及最重要的——可停靠楼层的不同。
1. 架构设计
-
类的组织:
本次作业与第二次的主要不同在于电梯种类,对于调度模式安排并没有改动
-
线程同步与锁:
为了模拟换乘效果(虽然不换乘也行),便会产生一个请求进出两次电梯的行为,要处理这类操作有很多种办法,我选择先让其进入电梯,等到达换乘楼层时便将其扔回
RequestList主请求队列。因此,线程之间的交互多出了一级:![]()
-
换乘方法:
电梯运行策略仍然没有修改,主要重构点在于调度器针对不同请求分配至不同的电梯;
在如何分辨和组织换乘人员上,我才用如下方法:
-
给每个请求增加一个参数
transfer;变量值 含义 0 该乘客不换乘(初始化即为0) i(1<=i<=20) 该乘客将要在i楼层换乘 -
对于要换乘的乘客,在调度器中手动为其设置
transfer参数,并将其扔给第一次进入的电梯; -
电梯接收到乘客请求时,若
transfer为0,则正常处理,否则,在transfer所处的楼层将其扔回主请求队列,同时将transfer参数置0; -
当然,调度器需要维护一个变量记录当前换乘出去但还没完全到达的请求的数目,当该数目为0时,才可安全结束调度器线程,否则可能结束了调度器却又接收到新的请求;
-
-
值得一提:
本次作业由于产生不同类型的加电梯请求,因此自然想到通过工厂模式,根据请求返回不同类型的电梯并设置不同的参数。
我采取的换乘策略比较死板(并没有实时根据当前电梯们的状态来决定是否换乘),因此为了简便将请求扔给哪一个电梯,我设置了三个电梯List来分别存放不同Type的电梯,并将 把请求扔给电梯 的行为作为一个方法独立出去。
2. 性能分析
-
代码长度:
代码实际长度567行,本次将不同Type的策略集中在调度器中,电梯只根据LOOK算法进行移动,降低了方法和类之间的耦合度。
-
方法复杂度:
仍然是延续第一次作业电梯类中两个方法较为复杂,本次作业调度器由于增加了不同Type的支持,也一定程度加大了复杂度,但总体情况良好
![]()
-
类图:
除了新增电梯工厂类
ElevatorFactory外,与第二次作业基本一致 -
性能:
尴尬的是,在提交完换乘版本后心血来潮一测试才发现之前的 不那么换乘的策略 效率似乎更高,可是已经逼近ddl不敢再次提交了。较为生硬的换乘策略导致本次测试在大量重复高层请求的情况下效果较差,有三个点80多分,其余较优都在99以上。
3. Bug分析
本次作业强测和互测都没有发现Bug。由于并没有线程安全上的改动,因此课下我自己并没有做更充分的测试,只是借助第二次作业的强测数据以及简易构造了换乘数据进行测试。
三、心得体会
(1)多线程问题
-
在本单元开始之前,我尝试通过pritf来模拟线程之间的协作,当时在
synchronized、wait、notify等操作之间纠结,直到后来我才理解了为什么要有锁、如何产生锁、如何释放锁的一系列问题。理清锁的原理和工作模式,对于写出结构更加完善稳定的程序带来了极大的帮助。 -
在第二次作业典型的普通多线程问题中,为避免线程安全问题,我仔细梳理了各个线程之间的交互方式:
-
输入线程与调度器线程共享主请求队列
-
调度器线程分别与各个电梯线程共享该电梯的等候队列
-
电梯维护自己的内部队列,电梯之间的运行逻辑互不干涉
这样的逻辑下,各线程出现死锁、深度睡眠、获取错误信息的可能性就大大降低了
-
-
多线程的学习进一步让我领会到了代码和机器执行的交互,与操作系统中进程、锁、原语等知识相呼应。遗憾的是,在完成当周作业之余,我并没有进一步探索更规范和高级的知识如线程池、
synchronizedCollection等,还有待更深入的研究学习。
(2)层次化设计
-
本单元的学习使我对于Java编程的大局观和细节处理更进一步:
大到类与类之间安排继承关系、线程之间交互关系,小到每一次获取锁、释放锁,以及思考调度算法的逻辑、思考电梯和调度器运行、关闭的每一个逻辑判断条件,都使我面对问题和面对程序更加冷静,充分思考。
-
回顾OO作业必定离不开优化的性能分
第一次作业中单纯的电梯调度算法并不是本单元的核心;第二三次作业,为合理安排多部电梯和尽可能多捎带,可能需要调度器根据目前各个电梯的运行状态(如方向、楼层)再判断具体将请求分给谁。但是,需要调度器遍历电梯并挨个判断,无论是否给电梯状态变量加锁(不知道有没有更高级的换乘策略),都会带来架构上的复杂性,也不太容易组织代码,于是我偷懒就直接让AB协作或AC协作。
虽然本单元作业我并没有花很多功夫在优化上,但整体而言,面对一个较复杂的工程问题时,比第一次作业体现出了更好的宏观思想,编写的代码也相对更加规范,有着显著的提升。
四、总结
随着本单元作业的结束,OO课程也过去了一半,希望在接下来的两单元作业中,能够再接再厉,跟随课程组的步伐,进一步体会面向对象程序设计的核心要素。









浙公网安备 33010602011771号