面向对象设计与构造第二单元总结

面向对象第二单元总结

一、同步块和锁的设计

三次作业均使用共享对象实现线程之间的交互,用sychronized加锁语句和wait-notifyAll模式实现同步控制。

第5次作业

本次作业我使用了2个线程:输入线程InputHandler和电梯线程Elevator。调度器Scheduler作为两个线程的共享对象。我把两个线程中请求输入和电梯运行的程序都放在了同步块中加锁保护。本次作业我的同步块和锁的设置比较混乱,最大的缺点是同步块规模太大,很多代码不涉及共享对象的访问,没有必要放在同步块中。电梯搭载乘客运行的过程中占据着Scheduler的锁,此时InputHandler无法向Scheduler传递新读入的请求,这种做法极大地减少了并发执行,降低了性能。

第6次作业

本次作业我使用了3组线程:输入线程InputHandler、电梯线程Elevator(多个)和调度器线程SchedulerInputHandlerScheduler之间用WaitQueue队列交互,SchedulerElevator之间用RequestQueue队列交互,每个电梯各自拥有一个队列。WaitQueueRequestQueue作为共享对象,在线程中使用时加锁保护。本次设计尽可能缩小sychronized同步块,只在线程访问共享对象时加锁,共享对象使用完毕立刻释放并notifyAll。防止出现一个线程长时间占据锁的情况。

实际上,相比于电梯开关门和上下楼的时间(即电梯线程sleep的时间),请求调度算法执行所消耗的时间是很少的,几乎可以忽略不计。所以,在第5次作业中只要把电梯运行的代码放在sychronized同步块之外就能达到很好的并发效果,充分利用电梯运行的时间进行调度。

第7次作业

本次作业沿用上次的同步块和锁的设计,只做了微小的改动。在第6次作业中,循环遍历RequestQueue的时候我采用如下的方式加锁:

for (int i = 0; i < eleNum; i++) {
    sychronized(requestQueues[i]) {
        //do something...
    }
}

然而,会出现如下问题:下一个共享队列获得同步锁之后上一个队列已经释放了同步锁,电梯访问并改变了上一个共享队列。这段代码是请求调度的一部分,要求在调度完成之前所有RequestQueue队列都不能被改变,所以这种加锁方式是不太合理的。尽管调度程序执行速度远快于电梯运行速度,上述问题出现的概率很小,而且出现了也不会造成最终结果错误,但这种方式是不合逻辑的,也会造成性能上的损失,所以我把sychronized语句加在循环之外,使整个循环都在同步块内执行。

二、调度器设计

第5次作业

  • 与其他线程的交互

    调度器Scheduler本身不是线程,而是作为InputHandlerElevator两个线程的共享对象存在。Scheduler自带一个请求队列instrQueueInputHandler接收输入并传给SchedulerScheduler分三种模式进行调度,并把请求加入队列。Elevator通过Scheduler提供的get方法从队首获取请求并执行。

  • 调度算法设计

    在本次作业中,我将PersonRequest请求拆分为了指令(Instruction)。一个Instruction包含了起始楼层、目的楼层、起始层进电梯的乘客id和目的层出电梯的乘客id。例如,假设电梯当前在a层,有乘客需要从b层前往c层,则需拆分出2个指令:电梯从a到b、电梯从b到c。电梯每次从instrQueue(上文称之为请求队列,事实上称其为指令队列更合适)中获取一条Instruction。这样拆分的优势在于电梯线程代码十分简洁;但是拆分请求和调度的任务都交给了Scheduler,同时也增加了Scheduler类的复杂度。综合下来很难判断是否真正简化了代码,这也是我在算法设计之前没有充分考虑的地方。

    调度分3中模式进行:

    • Random模式:InputHandler每次投放一个请求,遍历整个队列,寻找是否可捎带。若两个Instruction运行方向相同且路程有交集,则视为可捎带,根据实际情况将他们拆分为2-4个新的请求。
    • Morning模式:InputHandler每次投放6个请求,将6个请求按楼层从低到高排列之后转化为指令加入队列。
    • Night模式:由于所有请求同时到达,我使InputHandler将所有请求读入之后再传递给Scheduler,进行全局调度。在Night模式下实现了较好的性能。

    请求拆分和调度算法均由Scheduler类来实现,造成Scheduler比较臃肿,容易出现bug。

第6次作业

  • 与其他线程的交互

    本次作业中调度器Scheduler是一个线程。InputHandler将请求放入WaitQueue队列,SchedulerWaitQueue中读取请求进行调度再分配给电梯各自的RequestQueue

  • 调度算法设计

    由于上次作业采用的请求拆分为指令的思路造成Scheduler复杂度增加,本次作业不再使用Instruction,而是使用了一个RequestPackage类来存放多个请求(至多6个)。电梯每次执行读入一个RequestPackage,其中有多条请求,且都是可捎带的。RequestPackage维护一个候乘表,电梯每运行一层就查候乘表,看是否有乘客进出,由此决定是否在该层开关门。这样设计使电梯线程在保持简洁的操作的同事简化了Schduler,将部分功能交给底层的RequestPackage来实现。

    此外,本次作业使用了多部电梯,在调度的时候考虑均衡各个电梯的任务量比同意部电梯的捎带更加重要。我采用近似计算任务运行时间的方式实现调度。

    本次作业的调度算法合并了MorningNight模式。

    • Random模式:InputHandler每次投放一个请求,Scheduler遍历所有电梯的队列,计算各自队列中任务执行的时间,将请求分配给时间最短的电梯。将请求插入队列的时候再遍历该队列中的RequestPackage,查找能否进行捎带。
    • MorningNight模式:调度方法相对简单,InputHandler每次投放6个请求,Scheduler将6个请求打包(做成RequestPackage)插入执行时间最短的电梯对应的队列。

第7次作业

  • 与其他线程的交互

    本次作业沿用了第6次作业的架构,交互方式完全一致。

  • 调度算法设计

    由于电梯载客量不同,MorningNight模式下难以确定每次投放几个请求,所以我索性将三种模式合并为一种模式进行调度,即在任何模式下,InputHandler都是每次投放一个请求。Schduler每接收一个请求就把该请求作为最后一个请求,找到局部最优解。整体的调度思路仍然类似于第6次作业,但做了一些优化,计算更加精确。

    • 在计算电梯执行任务时间时加入电梯运行速度。
    • 电梯每次从队列中去除的RequestPackage不再是队首的一个,而是起点与电梯当前所在位置最接近的一个。
    • Scheduler每接收一个新的请求都采用模拟插入队列的方式,即依次假设放入各个电梯的RequestQueue,在捎带的情况下计算出系统总时间(执行时间最长的电梯的时间)和电梯运行总时间(所有电梯运行时间之和),以系统总时间为第一关键字、电梯运行总时间为第二关键字进行排序,找出最优的插入方式(有贪心的思想,近似认为找到了局部最优解)。
    • 没有采用换乘。因为换乘会引入等待时间,不能保证换乘一定优于不换乘。而且加入换乘会增加代码的复杂度,更容易产生bug。

三、架构设计及其可扩展性

第5次作业架构

  • UML类图

  • UML顺序图

  • 可扩展性分析

    正如前文中所述,本次作业的Scheduler类集中了过多的功能,显得臃肿。将请求转化为指令后调度更加复杂,如果增加多部电梯,则Schduler类的复杂度会有明显的上升。总之,本次作业的可扩展性不好。这也导致了我第6次作业的重构。

第6次作业架构

  • UML类图

  • UML顺序图

  • 可扩展性分析

    本次作业的设计具有较好的可扩展性,这也在完成第7次作业的过程中得到印证。

    • RequestPackage统一管理了能捎带的多个请求,可以根据电梯的载客量合理调整其容量。
    • 电梯运行时间的计算可以引入电梯速度,实现更精确的计算,便于应对多种型号电梯的调度。

第7次作业架构

  • UML类图

  • UML顺序图

    本次作业架构与第6次基本相同,UML顺序图相同

  • 可扩展性分析

    本次作业是基于上次作业的架构迭代而成,扩展性也比较好。

    • 新增更多类型的电梯只需在新建电梯时修改参数。
    • 我认为衡量本次作业的扩展性还需从换乘的角度着手。假设不存在全层电梯,则必须支持换乘机制。在这种情况下,可以为电梯的RequestQueue增加一个返回队列,当一位乘客下电梯但尚未到达目的地时加入返回队列,Scheduler从返回队列读取请求,分配给其他电梯。这样的设计可能需要加入是否需要换乘的判断方法。总体来看,原来的架构应该不会有较大的改动,主要工作在于新增换乘算法。原来的架构仍然是可扩展的。

四、自己程序的bug

第5次作业

类复杂度分析

Class OCavg OCmax WMC
Elevator 2.57 5 18
InputHandler 5 9 10
Instruction 1.07 2 16
MainClass 1 1 1
Scheduler 3 9 45

方法复杂度分析

Method CogC ev(G) iv(G) v(G)
Elevator.Elevator(Scheduler) 0 1 1 1
Elevator.doorClose() 1 1 2 2
Elevator.doorOpen() 1 1 2 2
Elevator.getOff() 3 1 3 3
Elevator.getOn() 3 1 3 3
Elevator.move() 1 1 2 2
Elevator.run() 12 3 7 7
InputHandler.InputHandler(Scheduler) 0 1 1 1
InputHandler.run() 22 3 11 11
Instruction.Instruction(int,int,int) 1 1 1 2
Instruction.addOff(int) 0 1 1 1
Instruction.addOn(int) 0 1 1 1
Instruction.getDirection() 0 1 1 1
Instruction.getFromFloor() 0 1 1 1
Instruction.getGetOff() 0 1 1 1
Instruction.getGetOn() 0 1 1 1
Instruction.getNum() 0 1 1 1
Instruction.getOffNum() 0 1 1 1
Instruction.getToFloor() 0 1 1 1
Instruction.setFromFloor(int) 0 1 1 1
Instruction.setGetOff(ArrayList) 0 1 1 1
Instruction.setGetOn(ArrayList) 0 1 1 1
Instruction.setToFloor(int) 0 1 1 1
Instruction.toString() 0 1 1 1
MainClass.main(String[]) 0 1 1 1
Scheduler.Scheduler() 0 1 1 1
Scheduler.carry(PersonRequest) 20 4 10 14
Scheduler.getFinish() 0 1 1 1
Scheduler.getInstr() 1 2 1 2
Scheduler.instrPoll() 1 2 2 2
Scheduler.isBetween(int,int,int) 3 1 1 4
Scheduler.isEmpty() 0 1 1 1
Scheduler.morningSchedule(ArrayList) 5 1 4 4
Scheduler.morningSort(ArrayList) 8 3 4 5
Scheduler.nightInstr(ArrayList) 9 1 5 5
Scheduler.nightSchedule(ArrayList) 5 1 4 4
Scheduler.prSort(ArrayList) 8 3 4 5
Scheduler.putInstr(PersonRequest) 2 1 2 2
Scheduler.requestToInstr(int,PersonRequest) 2 1 2 2
Scheduler.setFinish(boolean) 0 1 1 1

本次作业是出bug最严重的一次。主要原因在于架构设计的不合理以及同步块使用不合理。下面是bug分析。

  • 同步块的范围过大,没有遵循共享对象使用即加锁,用完即释放的原则,导致电梯运行期间始终占用Scheduler的锁,无法对 新读入的请求及时进行调度。所以,程序实际运行过程中捎带算法很少使用,性能太差,出现超时。由于本次作业捎带算法是针对Random模式的,所以超时均出现在Random模式下。这次bug让我知道应该尽可能缩小sychronized同步块的控制范围,及时释放锁,提高多线程的并发性。这次bug产生原因除了设计的不合理之外,还有测试的不充分。在自我测试的过程中我没有构造足够强的数据与官方ALS算法运行结果进行对比,没有意识到自身性能的严重不足。
  • 捎带函数在拆分请求时出错,出现了乘客在错误的楼层上电梯的情况。我认为主要原因是拆分请求的策略增大了调度算法的复杂度。例如,捎带函数需要考虑新请求与已有指令起点终点是否重合等多种情况,捎带函数复杂度太高,容易出现bug。另外,如上文所述,加锁范围过大,捎带函数很少使用,在中测和自测中几乎没有用到捎带函数,导致该bug被隐藏起来,难以发现。

bug出现在Scheduler类中,如上表所示,其加权方法复杂度最高(45)。具体到方法,则bug出现在复杂度最高的carry方法中。

第6次作业

类复杂度分析

Class OCavg OCmax WMC
Elevator 3.14 7 22
Entry 1 1 6
InputHandler 2 3 4
MainClass 3 3 3
RequestPackage 1.75 3 14
RequestQueue 1.67 4 25
Scheduler 3.38 9 27
WaitQueue 1 1 9

方法复杂度分析

Method CogC ev(G) iv(G) v(G)
Elevator.Elevator(RequestQueue) 0 1 1 1
Elevator.doorClose() 1 1 2 2
Elevator.doorOpen() 1 1 2 2
Elevator.excute() 1 1 2 2
Elevator.funcFloor() 5 1 4 4
Elevator.moveTo(int) 5 1 3 4
Elevator.run() 13 7 8 9
Entry.addIn(int) 0 1 1 1
Entry.addOut(int) 0 1 1 1
Entry.getEnter() 0 1 1 1
Entry.getOuter() 0 1 1 1
Entry.isEmpty() 1 1 2 2
Entry.needStop() 0 1 1 1
InputHandler.InputHandler(WaitQueue) 0 1 1 1
InputHandler.run() 4 3 4 4
MainClass.main(String[]) 2 1 3 3
RequestPackage.RequestPackage(PersonRequest) 2 1 2 3
RequestPackage.add(PersonRequest) 8 1 1 9
RequestPackage.getBegin() 0 1 1 1
RequestPackage.getDirection() 0 1 1 1
RequestPackage.getEnd() 0 1 1 1
RequestPackage.getMap() 0 1 1 1
RequestPackage.getReqNum() 0 1 1 1
RequestPackage.getTime() 3 1 2 3
RequestQueue.RequestQueue() 0 1 1 1
RequestQueue.add(RequestPackage) 0 1 1 1
RequestQueue.canAdd(PersonRequest,RequestPackage) 4 2 4 5
RequestQueue.get() 1 2 2 2
RequestQueue.getBusy() 0 1 1 1
RequestQueue.getCurrentFloor() 1 2 2 2
RequestQueue.getId() 0 1 1 1
RequestQueue.getIsEnd() 0 1 1 1
RequestQueue.getQueue() 0 1 1 1
RequestQueue.insert(PersonRequest) 4 3 4 4
RequestQueue.isEmpty() 0 1 1 1
RequestQueue.setBusy(boolean) 1 1 1 2
RequestQueue.setEnd(boolean) 0 1 1 1
RequestQueue.setId(int) 0 1 1 1
RequestQueue.waitTime() 3 1 3 3
Scheduler.Scheduler(WaitQueue,RequestQueue[]) 0 1 1 1
Scheduler.addElevator(ElevatorRequest) 0 1 1 1
Scheduler.dispatch(ArrayList) 8 1 4 5
Scheduler.dispatch(PersonRequest) 3 1 3 3
Scheduler.getFinish() 0 1 1 1
Scheduler.randomRun() 13 4 6 6
Scheduler.run() 26 5 10 10
Scheduler.setFinish(boolean) 0 1 1 1
WaitQueue.WaitQueue() 0 1 1 1
WaitQueue.add(Request) 0 1 1 1
WaitQueue.get() 0 1 1 1
WaitQueue.getAll() 0 1 1 1
WaitQueue.getEnd() 0 1 1 1
WaitQueue.getPattern() 0 1 1 1
WaitQueue.isEmpty() 0 1 1 1
WaitQueue.setEnd(boolean) 0 1 1 1
WaitQueue.setPattern(String) 0 1 1 1

本次作业出现了一个超时的bug,即强测的data8和data9。原因是调度算法优化不足,没有做充分的捎带。另外,这两个数据点本身对时间性能的要求比较高,需要我们做充分的捎带。我在第7次作业中使用了更精确的时间计算方式,求得局部最优解。将低7次的优化算法移植到第6次作业后修复了该bug。实际上,第5次作业的bug也是用第6次的架构简化之后修复的。

该bug出现在RequestQueue中,其复杂度较高,但没有超标。

第7次作业

由于近似实现了局部最优解(无换乘),本次强测和互测没有出现bug。

类复杂度

Class OCavg OCmax WMC
Elevator 3.29 8 23
Entry 1 1 5
InputHandler 2 3 4
MainClass 3 3 3
RequestPackage 2.33 7 21
RequestQueue 2.11 6 40
Scheduler 3 6 18
WaitQueue 1 1 8

方法复杂度

Method CogC ev(G) iv(G) v(G)
Elevator.Elevator(RequestQueue) 0 1 1 1
Elevator.doorClose() 1 1 2 2
Elevator.doorOpen() 1 1 2 2
Elevator.excute() 1 1 2 2
Elevator.funcFloor() 5 1 4 4
Elevator.moveTo(int) 5 1 3 4
Elevator.run() 15 7 9 10
Entry.addIn(int) 0 1 1 1
Entry.addOut(int) 0 1 1 1
Entry.getEnter() 0 1 1 1
Entry.getOuter() 0 1 1 1
Entry.isEmpty() 1 1 2 2
InputHandler.InputHandler(WaitQueue) 0 1 1 1
InputHandler.run() 4 3 4 4
MainClass.main(String[]) 2 1 3 3
RequestPackage.RequestPackage(PersonRequest) 2 1 2 3
RequestPackage.add(PersonRequest) 8 1 1 9
RequestPackage.getBegin() 0 1 1 1
RequestPackage.getDirection() 0 1 1 1
RequestPackage.getEnd() 0 1 1 1
RequestPackage.getMap() 0 1 1 1
RequestPackage.getReqNum() 0 1 1 1
RequestPackage.getTime(int) 3 1 2 3
RequestPackage.removeReq(int) 9 5 4 7
RequestQueue.RequestQueue() 0 1 1 1
RequestQueue.add(RequestPackage) 0 1 1 1
RequestQueue.canAdd(PersonRequest,RequestPackage) 5 2 4 5
RequestQueue.get() 6 2 3 4
RequestQueue.getHashSet() 0 1 1 1
RequestQueue.getId() 0 1 1 1
RequestQueue.getIsEnd() 0 1 1 1
RequestQueue.getMaxNum() 0 1 1 1
RequestQueue.getSpeed() 0 1 1 1
RequestQueue.getTime(PersonRequest) 3 2 3 3
RequestQueue.insert(PersonRequest) 4 3 4 4
RequestQueue.isBetween(int,int,int) 3 1 1 4
RequestQueue.isEmpty() 0 1 1 1
RequestQueue.removePac(int,int) 2 1 2 2
RequestQueue.setBusy(boolean) 1 1 1 2
RequestQueue.setEnd(boolean) 0 1 1 1
RequestQueue.setId(int) 0 1 1 1
RequestQueue.setType(String) 9 1 6 6
RequestQueue.waitTime() 15 1 6 6
Scheduler.Scheduler(WaitQueue,RequestQueue[]) 0 1 1 1
Scheduler.addElevator(ElevatorRequest) 0 1 1 1
Scheduler.calSysTime(int) 1 2 1 2
Scheduler.dispatch(PersonRequest) 8 1 3 7
Scheduler.run() 2 1 3 3
Scheduler.runScheduler() 13 4 6 6
WaitQueue.WaitQueue() 0 1 1 1
WaitQueue.add(Request) 0 1 1 1
WaitQueue.get() 0 1 1 1
WaitQueue.getEnd() 0 1 1 1
WaitQueue.getPattern() 0 1 1 1
WaitQueue.isEmpty() 0 1 1 1
WaitQueue.setEnd(boolean) 0 1 1 1
WaitQueue.setPattern(String) 0 1 1 1

本次作业RequestQueue类复杂度相较于第6次作业有所增加,这是由于调度算法时间计算更精确造成的,好在没有导致新的bug。

五、测试他人程序的方法

本单元作业我在互测中没有发现他人的bug。我想相信bug是必然存在的,只是测试强度不够。我用自测的数据在本地做了少量的测试,方法十分简单

  • 打包jar,编写C程序实现按时间戳输入,借助命令行管道运行(参看讨论区)。
  • 编写C程序,采用随机数生成出发和到达楼层(MorningNight模式固定其中一个),产生测试数据。

本次互测没有发现线程安全问题。我自己的程序所幸也没有出现线程安全问题,因为我全部使用sychronized加wait-notifyAll模式,凡是用到共享对象都简单粗暴加锁保护,共享对象较少,任何两个线程之间最多有一个共享对象,不太可能出现死锁。

六、心得体会

关于线程安全

线程安全问题出现的原因主要是不同线程之间访问了同一个对象或变量,对其进行了修改。导致不同执行顺序下结果不同的情况。本单元电梯作业都是通过少数几个共享对象(如队列、调度器)实现线程之间的交互。我以简单的sychronized加锁方式避免线程安全问题。各个线程中凡是访问了共享对象的地方我都加了sychronized保护,不区分读写,以少量的性能损失换取线程安全。

本单元我出现的bug都是调度设计的不完善造成的,不涉及线程安全问题。然而,线程安全是多线程编程中最重要的问题之一,决不能因为自己的程序中恰好没有出现而掉以轻心。

关于层次化设计

本单元的作业让我深刻体会到了顶层架构设计的重要性。第5次作业不合理的架构导致编写代码耗费了大量的时间而且效果很差,事倍功半。相比之下,第6次作业进行了重构,代码编写顺利了很多,并且第7次作业也能沿用第6次的架构,只做了少量的改动,这也使得第7次作业成为我目前为止完成最快的一次OO作业。第6、7次作业我主要从以下几个层面进行设计。

  • 顶层架构:参考了一次课上上机的架构,将Scheduler作为一个单独的线程,依靠两级队列实现信息的交互,采用分布式调度。虽然延长了执行的流程,但使得每一步都得到了简化,也使得优化更加容易。
  • 电梯执行:电梯以RequestPackage为单位执行任务,电梯执行任务期间不与外界进行交互,捎带策略由Schduler完成,降低了电梯与其他部分的耦合性,减少了出现线程安全问题的可能性。
  • 调度策略:分为分配和捎带,先将请求分配给电梯各自的队列,再由队列内的方法实现捎带。

本单元的作业既有失败的教训,也有成功的经验。我从以下几方面总结一下自己的感悟。

  • 首先强调的还是良好的架构设计,在动手编写代码之前要做好规划(在第一单元也深有体会)。设计的层面不能仅局限于顶层架构。有时候按日常生活中的思路来分析顶层架构感觉不错,但涉及具体实现就会遇到很大的困难(例如代码及其繁琐,甚至某些功能无法实现)。以本单元作业为例,我认为写代码之前架构设计应该至少涵盖上面总结的3个部分:不仅要确定使用哪些类、哪些线程,还要对电梯如何运行,调度算法如何设计有比较清晰的认识。
  • 自我测试非常重要!总体来说,本单元作业我在自测和互测中都做得不够。例如第5次作业,在通过中测之后我就有了安全感,只是简单实验了几个数据就懒得再做强化测试,结果有严重的bug没有发现。这是一个深刻的教训!今后的程序设计中一定要做充分的测试,而且尽量是自动化测试。从第7次作业开始,我尝试了使用C程序有目的地生成一些测试数据,但没有尝试批量测试,还做得远远不够。今后应该督促自己在程序测试上下更多的功夫。
posted @ 2021-04-27 00:15  李雨东  阅读(52)  评论(0)    收藏  举报