目录
目录一、第一次作业分析设计策略基于度量分析程序结构二、第二次作业分析设计策略基于度量分析程序结构三、第三次作业分析设计策略基于度量分析程序结构四、分析自己程序的bug五、发现别人程序bug所采用的策略六、心得体会线程安全设计原则
一、第一次作业分析
设计策略
采用生产者-消费者模式,调度器作为托盘,请求处理器不断向托盘提交请求,电梯不断从托盘中获取请求执行,当托盘为空时,进入等待。其中调度器是共享对象,请求处理器和电梯是两个线程。
线程安全方面,由于理论知识不扎实,由请求处理器和电梯分别管理了自己的线程安全问题。
基于度量分析程序结构
1. 代码规模
2. 经典OO度量
- 方法复杂度
ev(G) | iv(G) | v(G) | |
---|---|---|---|
Total | 24 | 30 | 32 |
从中可看出,两个线程的run()
方法复杂度都较高,分析其原因是因为在run()
方法中对线程安全进行了管理。
- 类复杂度
WMC:类方法加权衡量(类中方法数)
OCavg:类平均循环复杂度
各类方法数差距不大。电梯方法数最多是因为将电梯运行拆分成了各个模块(开门、上下人、关门)的行为。
3. 类图
- 优点:运用了生产者、消费者模式,将调度器作为托盘的模式将请求输入模拟器与电梯的耦合度降至最低
- 缺点:两个线程分别考虑自己的线程安全问题,存在“多管闲事”的行为,应交由调度器统一管理,线程只负责自己的行为
4. UML时序图
5. SOILD设计原则
- SRP:符合
- OCP:未涉及增添新功能
- LSP:符合
- ISP:未涉及接口使用
- DIP:未涉及
二、第二次作业分析
设计策略
基本上仍然沿用第一次作业中生产者-消费者的模式,请求进入请求处理器后被送入调度器,调度器再进行处理后分配。本次在ALS的基础上增加了自己的优化,即将电梯第一目标是主请求优化为在运行过程中变更目的楼层,思路是接送可接送的最大请求数,有些类似于LOOK算法。
调度器取到请求时先存放入缓存区,电梯索要捎带队列时,调度器根据电梯状态,检索缓存区和请求仓库,分配合适的请求,并将缓存区的请求flush入请求仓库。请求仓库相当于等待队列,缓存区相当于刚来的等待队列,分配的请求相当于即将进入电梯的队列。
电梯无等待队列,分为待进入和待走出两个队列,取到的捎带队列放入待进入队列,只要待进入队列中有请求处于电梯当前所在楼层,就将其送入电梯。电梯线程仅需根据待进入和待走出两个队列设定自己的方向、目标楼层,不停运输请求即可。
基于度量分析程序结构
1. 代码规模
2. 经典OO度量
- 方法复杂度
ev(G) | iv(G) | v(G) | |
---|---|---|---|
Total | 47 | 78 | 91 |
从整体可看出,整个设计的圈复杂度(v(G))最大,模块设计复杂度(iv(G))次之,基本复杂度(e(G))最小,说明代码的结构化程度整体较好,但模块中条件语句泛滥,模块间调用关系也较繁多。
分析单个模块,QueueController.requestTakeable()
的复杂度最“突出”,是因为在这个函数中传入了电梯的当前状态(所处楼层、方向、目标楼层),用于判断捎带队列是否可捎带。当时在完成这一函数时,由于是为实现功能而不断添添补补,导致模块逻辑混乱,毫无设计可言。
对于Elevator
类中的getFinalFloor()
,checkPersonInandOut()
等方法中,也是由于过度看重功能的实现,而忽略了对模块高内聚、低耦合的设计目的进行检查。
- 类复杂度
电梯中的方法权重过大,虽是按照功能划分了不同的方法模块,但由于考虑不周,产生了一些没有必要的模块划分。也可能是由于职能分配不周,让电梯多做了些事情。
3. 类图
- 优点:保证捎带正确的基础上,改进的电梯调度比原生ALS调度算法快了些。
- 缺点:设计与实现的不统一。在实现的过程中由于设计的某些偏差,又重新更改设计,进行增添修改,造成逻辑混乱、调试困难。即使保证了正确性,却很难再进行扩展。
4. UML时序图
5. SOILD设计原则
- SRP:不符合。调度这一职能在调度器和电梯中被分割执行,调度器先根据状态分配捎带队列,电梯再根据自己的队列和捎带队列确定状态,二者都在完成调度这一功能;且电梯中入
personInandOut()
等方法也涉嫌“多管闲事”(负责开关门、取捎带队列等) - OCP:不符合。改变调度策略的同时,电梯中的运行方法也进行了大幅度修改。
- LSP:符合
- ISP:未涉及接口使用
- DIP:未涉及
三、第三次作业分析
设计策略
采用了Worker-Thread模式,但有一点不同,需求request没有execute()
方法,而是交由工人(电梯)来完成需求。本次设计还采用了观察者模式中的一些思路,即电梯可以加入/退出调度器,支持电梯数量的动态变化,且调度器中有新需求(更新)时,会通知观察者(电梯)们。
请求输入处理器与前两次作业基本相同,作出的改进是由调度器管理线程安全。
调度器中存放三部电梯的可达楼层表,以及与三部电梯相对应的三个请求队列。本次还将请求抽象为乘客的成员,乘客还有自己的属性:直达或是换乘。调度器通过判断乘客请求,将其标记为直达或换乘,若为直达,分配到对应可直达电梯;若为换乘,分配到可到达其出发楼层的电梯。电梯的乘客分配优先级为A>B>C,此次没有想出能同时保证线程安全和性能的优先级变更策略。
电梯中有两个队列:等待队列和在电梯中的队列。通过两个队列确定电梯的运行方向、目标楼层,调度完全在电梯中进行。换乘采用的策略是,若电梯内部乘客是换乘的乘客,则在其他两部电梯的可达楼层中找“港口”(即电梯当前楼层是某一电梯的可达楼层),若当前楼层是“港口”则把乘客送出电梯,向调度器送入新请求,请求出发楼层是当前楼层,目标楼层是乘客目标楼层,此时总有一部电梯能让乘客直达目标楼层。这种策略简化了电梯换乘应考虑的线程安全问题,但不易进行优化。
三部电梯获取乘客的线程安全问题由调度器解决,调度器中三部电梯对于的请求队列若有更新,则通知对应的电梯。除此之外电梯独立运行,互不干扰。
基于度量分析程序结构
1. 代码规模
2. 经典OO度量
- 方法复杂度
ev(G) | iv(G) | v(G) | |
---|---|---|---|
Total | 76.0 | 121.0 | 137.0 |
对于基本复杂度ev(G),有两个方法出现“赤字”,分别是Elevator.checkTransfer()
(ev(G)=4)和Scheduler.putRequest()
(ev(G)=5)。对于Elevator.checkTransfer()
,由于使用了存放在调度器中的电梯可达楼层表floorMap
,用于判断乘客是否可在当前楼层换乘,故结构化程度有所降低。Scheduler.putRequest()
中使用Passenger
类创建了乘客,并两次遍历电梯可达楼层,第一次找直达电梯,若没有,第二次找换乘电梯。模块略显臃肿,结构化程度低。
对于模块设计复杂度iv(G),Elevator.getAimFloorThroughList()
(iv(G)=9)出现了“赤字”。这一方法功能为通过电梯的两条队列,寻找最终的目标楼层(即寻找可接送的最高楼层)。在方法中调用了大量类的get方法获取private属性(Passenger.getKind()
,PersonRequest.getTo/FromFloor()
等),以及队列的isEmpty()
,size()
等方法,还调用了电梯自己的子方法寻找楼层,模块间耦合程度过高。
对于圈复杂度v(G),各个大的功能模块数值都较大,其中判断结构较多,但没有“赤字”出现。
- 类复杂度
电梯中的方法仍然过于臃肿,原因是因为调度完全交由电梯管理,电梯中包含有调度、改变自身状态、询问乘客上/下电梯等功能的方法
3. 类图
- 优点:使用了Vector,CopyOnWriteArrayList等线程安全的容器,各个模块分工明确:请求处理器接受请求传入调度器,调度器负责请求的分配、电梯启动的控制,电梯负责运输乘客
- 缺点:电梯模块过于臃肿,有“神仙类”与“傻瓜类”并存的嫌疑。结构分层没有仔细考虑,仍使用不带包结构、类能省则省的设计策略进行设计。
4. UML时序图
5. SOILD设计原则
- SRP:基本符合,类和方法的功能在设计时进行了区分,但内部可能有些细节在实现时添入而破坏了原则
- OCP:不符合,抛弃了前两次作业,进行了重构
- LSP:符合
- ISP:未涉及接口使用
- DIP:未涉及,若电梯种类不同则需考虑
四、分析自己程序的bug
本次电梯系列作业,三次作业的强测与互测中都没有被发现bug。
在第一次作业中,课下寻找自己程序的bug采用的是IDEA自带的多线程debug。
赋予线程名字,在想要查看的线程中设置断点进行单步调试,分析线程的行为。但一次只能进入一个线程进行调试,对于多个线程的状态,使用Jprofiler进行查看。
第二次作业中仍采用上述方法,主要是对调度器分配捎带队列的行为和电梯自身调度行为进行检查,所以比较有效,但由于设计不好,导致de出一个bug长出两个的窘况。此外,在第二次互测期间还实现了定点投放的python脚本(在“发现别人bug采用的策略“一栏中给出源码)。
第三次作业由于设计花费时间较多,所以实现时逻辑较清晰,采用的是”先解决线程安全问题,再实现功能“的策略,同一时间只解决一个类型的问题。调试方法采用的也是第一第二次作业中的方法。
五、发现别人程序bug所采用的策略
三次作业的互测环节0战绩,问题在于不知道往哪个方向攻破别人的代码,线程安全与调度问题似乎处理得都挺好。此外,多线程bug难以复现也是问题之一。
本单元测试策略与第一单元的差异在于,测试的重心由输入输出边界转变为了线程安全与调度,更多的考虑整体设计、功能性能,而不是钻牛角尖,卡边角。
第一次作业较为简单,着重点在于检查线程安全问题,但没有收获。
第二、第三次作业使用了自己的定点投放脚本,将自己课下积累的数据逐一投放,统计互测屋中所有人的电梯运行时间,若平均运行时间较长,从代码中寻找漏洞;若平均运行时间较短,参考他的代码并汲取精华。
下面放出定点投放的脚本(多组同时投放的数据会出现bug):
#!/bin/bash
# coding=utf-8
import subprocess
import time
import linecache
import re
import signal
def showInFile(infilename, sourcename):
#"warout/"是每个人的输出结果
outfilename = "warout/" + sourcename + ".txt"
with open(outfilename, "w") as outfile:
#开启子进程,通过命令行打开jar包
cmd = "java -jar src/" + sourcename + "/out/" + sourcename + ".jar"
print("------------------------------------------")
print(cmd, end='\n\n')
res = subprocess.Popen(cmd, shell=True, stdin=subprocess.PIPE,
stdout=outfile.fileno(), stderr=subprocess.PIPE)
#投放数据
infile = open(infilename, "r")
start = time.time()
castData(infile, start, res)
res.communicate()
#取最后一行输出
finalout = open(outfilename, "r")
print("last line : " + finalout.readlines()[-1])
print('end\n')
def castData(infile, starttime, res):
for line in infile:
instr = line
if (line == ""):
print(res.poll())
if (line == '\n'):
continue
# get input time
intime = re.findall(r"\[(.+?)\]", instr)
intime = float(intime[0])
# get request
instr = instr.replace('[' + str(intime) + ']', "")
# sleep until specific time
span = time.time() - starttime
shouldsleep = intime - span
if (shouldsleep < 0 and intime != 0):
print("error input!")
continue
else:
time.sleep(abs(shouldsleep))
# debug note
print("at " + str(time.time() - starttime))
# print(line)
print(line, end='')
# send data into stdin
res.stdin.write(bytes(instr, 'utf-8'))
res.stdin.flush()
print("flush all\n")
if __name__ == '__main__':
showInFile("input.txt", "saber")
showInFile("input.txt", "rider")
showInFile("input.txt", "lancer")
showInFile("input.txt", "berserker")
showInFile("input.txt", "assassin")
#showInFile("input.txt", "archer")
showInFile("input.txt", "caster")
showInFile("input.txt", "alterego")
showInFile("input.txt", "kk") #本人
print("all done")
六、心得体会
线程安全
在线程安全方面,本单元作业算是一个入门。只停留在synchronized锁对象的层面上,对于一些高阶的线程安全处理方法还不了解。
第一次和第二次作业仅需处理请求处理器和电梯间的线程安全问题,做法是在线程内部独自处理。请求处理器接受到请求时,通过共享对象——调度器进行notify,电梯不断运行,处理完当前请求队列后获取调度器中的请求,若没有则通过调度器wait,释放锁。第二次作业还需考虑获取捎带队列的情况,解决办法为电梯进行一次服务时,只有第一次取请求队列时判断是否等待,其余时刻取捎带队列时,即使捎带队列为空也不会等待。
第三次作业中涉及了四个线程:一个请求处理器和三部电梯,虽然看似使线程安全问题复杂了,但仔细想,只要能够在调度器中完成请求的分配,在电梯取请求时控制好线程安全,电梯运行时完全独立,就解决了问题。
还有一方面需要思考的,是换乘问题,最初考虑使用Passenger
类存放该乘客的换乘步骤,但对于换乘请求的分解没有想到好的对策,既能解决自动设置步骤,又能保证可扩展性,且分解请求的做法还需考虑请求执行的先后顺序,又是一大线程安全问题。在查看互测屋代码与同袍交流的过程中,发现有同学使用枚举的办法进行换乘请求分配,或许也是一个不错的选择,只是如果有新的电梯加入时,需要人工进行换乘情况的添加与修改。个人对换乘问题,采用的是寻找港口的办法,如果有必须换乘的乘客,电梯直观在出发楼层将他接入,在运行过程中找到能够让他乘坐其他电梯直达目的楼层的“港口”,就将其请求处理后送回调度器,这样就无需考虑步骤的先后问题,电梯还是在独立运行,换乘的乘客请求交由调度器处理,绕开了三部电梯间的线程安全问题。
总结本单元作业,基本的线程安全问题能够使用synchronized,notify,wait等常见处理方法解决,但对于其他线程安全控制办法并没有太多了解,还需在后续阅读书籍、博客以及练习中提升。
设计原则
本单元作业,体会最深的是,将编程这项工作从算法层上升到了设计层,更强调整体的框架设计,而不是停留在单个模块的性能优化。从此才体会到,为什么有人说“编程是门艺术”。好的架构设计,让人看一眼就觉得充满美感,而杂乱无章的设计,则使人心情烦躁。
前两次作业由于没有了解到“SOLID”设计原则,对整个框架的设计还停留在为实现功能而设计的层面上。设计模式采用的是生产者-消费者模式,生产消费关系比较清晰,但内部模块设计只为完成功能,没有考虑设计原则。
第三次作业前的那一堂课,了解到了原来面向对象程序设计还有这样那样的设计原则,再反思自己从前写过的代码,仿佛是一堆废品。第三次作业在Worker-Thread设计模式的基础上,考虑了整个架构的模块职能分配、可扩展性等等。最终完成的设计可允许添加更多电梯,也可以进行电梯请求分配优先级调整、电梯内部调度调整等优化,但本人没有想好即保证平均运行时间提升又不破坏框架的优化方法,所以仍沿用第二次作业的调度策略,稍作修改。
本单元作业还有些遗憾,就是没有使用多态、继承、接口等面向对象的特性,里氏原则、ISP原则也无法得到运用,作业显得没那么“面向对象”了。希望在后续单元的练习中,能够考虑得更仔细周到,运用多态、继承与接口最大化实现代码复用,使得设计结构抽象更加合理。此外,还需通过学习Java设计模式,在不同场景中灵活运用,解决不同的实际问题,不断提升自己的面向对象程序设计能力。