OO 第二单元总结
OO 第二单元总结
一、总述
在本单元中我们初步学习了多线程的知识,熟悉实现线程安全的方法,逐步迭代开发实现了电梯调度系统。
UML协作图如下:
二、作业分析
1. 第五次作业
(1)作业要求
本次作业要求较为基础,目的在于正确使用多线程写出线程安全的代码,仅要求在五个楼座中每栋楼实现一个电梯,来处理本楼座的请求,无跨楼座要求,时间基准为ALS捎带算法,限制了电梯调度算法性能。
(2)具体实现
本次作业我参考实验样例代码完成,使用了三个线程来实现需求,输入线程、电梯线程以及调度器线程(非必须),在自己架构时并未考虑设计调度器线程,而是作为类来实现,但由于涉及分配以及终止等需求(主要是可以白嫖),因此重新按照三个线程架构。
-
MainClass负责启动输入线程、调度器线程以及电梯线程,PersonRequest调用了官方包并未自己构建,读入线程负责读入请求并放入总体的等待队列。调度器负责将waitQueue中的请求加入到各电梯的等待队列elevatorQueue中,电梯仅需就当前运行状态及自身elevatorQueue中的请求来决定运行策略。
-
电梯运行策略我选择了Look算法,但并不纯粹,策略方面并不存在某种算法最优,而是根据不同特点有不同的侧重,起初我设计了标准的Look电梯,但由于无论有无需要电梯一直运行,一方面违背常识,另一方面在每一层输出的arrive信息消耗大量时长,于是更改为具有ALS特点的Look电梯。
-
电梯没人且没请求时停在原地等候(!!!注意避免轮询)
-
电梯当前没人有新请求则前往新请求方向接人,期间能捎带则捎带,但在新请求与其反向时,不能超过新请求所在位置(确保新请求被响应)
-
电梯有人时同方向的捎带,在一个方向上运行时全部乘客离开则进入原地待命
-
-
关于线程安全方面,本次作业将电梯等待队列作为共享对象实现共享队列类,该类中方法全部加锁,这样所有锁均在该类中获得与释放,确保了线程的安全。本次作业中遇到的线程安全问题并不多,因为仅有一部电梯访问,且锁均加在共享对象上,不会产生死锁。但在优化时产生了轮询现象,由于理解不够深刻,浪费了很多提交机会,最后妥协采取了sleep(50)的方法来避免轮询。
-
bug修复:本次作业忽视了输出线程不安全的问题,索性强测并未测试,在修复阶段使用单例模式封装成一个输出类从而避免了同时访问官方包的问题。此外还解决了轮询的问题,在look算法中加锁,若需要等待,则wait()释放锁,等待有新乘客加入再唤醒线程。
(3)代码复杂度分析
Method | CogC | ev(G) | iv(G) | v(G) |
---|---|---|---|---|
Controller.Controller(RequestQueue, HashMap<Character, RequestQueue>) | 0 | 1 | 1 | 1 |
Controller.run() | 11 | 4 | 5 | 11 |
Elevator.Elevator(char, RequestQueue) | 0 | 1 | 1 | 1 |
Elevator.addPerson(PersonRequest) | 7 | 5 | 5 | 5 |
Elevator.close() | 2 | 1 | 3 | 3 |
Elevator.getPersonIn(ArrayList<PersonRequest>) | 18 | 3 | 6 | 10 |
Elevator.look() | 13 | 2 | 8 | 11 |
Elevator.open(ArrayList<PersonRequest>, ArrayList<PersonRequest>) | 3 | 1 | 4 | 4 |
Elevator.run() | 9 | 3 | 6 | 7 |
InputHandler.InputHandler(RequestQueue) | 0 | 1 | 1 | 1 |
InputHandler.run() | 5 | 3 | 4 | 4 |
MainClass.main(String[]) | 1 | 1 | 2 | 2 |
OutPut.println(String) | 0 | 1 | 1 | 1 |
RequestQueue.RequestQueue() | 0 | 1 | 1 | 1 |
RequestQueue.addRequest(PersonRequest) | 0 | 1 | 1 | 1 |
RequestQueue.getOneRequest(int) | 5 | 2 | 4 | 5 |
RequestQueue.getPersonRequests() | 0 | 1 | 1 | 1 |
RequestQueue.isEmpty() | 0 | 1 | 1 | 1 |
RequestQueue.isEnd() | 0 | 1 | 1 | 1 |
RequestQueue.setEnd(boolean) | 0 | 1 | 1 | 1 |
Class | OCavg | OCmax | WMC | |
Controller | 6 | 11 | 12 | |
Elevator | 4.57 | 9 | 32 | |
InputHandler | 2 | 3 | 4 | |
MainClass | 2 | 2 | 2 | |
OutPut | 1 | 1 | 1 | |
RequestQueue | 1.29 | 3 | 9 | |
由表格可见,电梯调度相关算法的复杂度较高,调度器线程和电梯线程的run方法复杂度也较高(可能是因为while(true)???)
(4)bug分析 & hack策略
本次作业中没有封装输出安全,所幸强测并没有测试输出线程安全的问题,但在互测中被狂砍几十刀,我也丢了一些边界数据,似乎也恰好由于线程安全问题hack到了其他人。
2. 第六次作业
(1)作业要求
本次作业要求在第五次作业的基础上增量开发,响应动态增加电梯的请求,并增设横向电梯来满足同层不同座的请求,相对而言较往年改动量更多(题目预测失败)。
(2)具体实现
基于第一次作业,大概提出了这样几种构想:
-
自由竞争:
采取自由竞争的方式,同一座的所有电梯(最多三个)共享一个队列,在输入线程里响应增加电梯的请求,并且将该座的队列分配给新电梯。遇到新请求时,多电梯同时响应,以最先接到的电梯为主。但要注意多电梯共享一个队列,要注意互斥共享。
同层实现方法类似,单独开一个每层(横向)的请求队列,该层横向电梯共享,依然采用自由竞争的方式。
由于电梯间共享队列,因此要注意线程问题,每个电梯访问时要注意加锁,但是由于其他电梯的访问,可能不能实时的获得锁,因此可能对性能有一定影响,同时陪跑也会占用输出时间。
-
统一调度:
在调度时统一分配好,每个电梯有独立的请求队列,这种情况和第一次作业类似,不涉及电梯间的共享,线程方面更为安全。横向电梯类似。
该种架构下分配方式值得仔细设计,因为随机分配可能并不能得到较优的性能,因此如何分配是个难题。可以粗略地基于方向,距离,电梯内人数来做出分配,但并不一定可以精准模拟。后来经吕诚鑫同学提示(抱大腿),可以在电梯中仿照run方法,模拟一个接受请求后到成功处理的时间,挑出时间最短的电梯进行分配。在我自己尝试的时候发现,想要获得精准的模拟结果,就要实时的获得请求队列,这一点上也需要获得锁,因此也需要等待,此外,考虑到最多只有三个电梯共享同一队列,因此自由竞争并不一定性能会差(
手动模拟太过复杂),因此经过反复横跳后又改回自由竞争。
但统一调度胜在线程安全,也更合乎常理,此外,自由竞争时的陪跑可能要输出多余的arrive信息,也会消耗一定时间,因此一种较好的统一调度方式或许会有更优的性能。
本次纵向电梯改成了纯正的look,横向电梯仿照look写了移动策略,整体代码量并没有很大,本次接人增添了这样的优化:由于电梯负载能力有限,因此在同一层有很多请求时捎带哪些请求会影响性能。一方面,捎带目的地近的可以保证电梯可以较早腾出空间以便容纳更多请求;另一方面,稍带目的地远的,可以跑得尽可能远,而减少折返。权衡之下,本次选择了后者,优化后在面对大数据时电梯性能得到了飙升。
关于线程安全方面,本次采取了自由竞争的调度方式,同座(层)电梯间共享请求队列,因此需要特别注意读写的互斥访问。在需要套锁时套好锁,确保线程的安全。
(3)代码复杂度分析
Method | CogC | ev(G) | iv(G) | v(G) |
---|---|---|---|---|
Controller.Controller(RequestQueue, HashMap<Character, RequestQueue>, ArrayList<RequestQueue>) | 0 | 1 | 1 | 1 |
Controller.run() | 14 | 5 | 7 | 8 |
Elevator.Elevator(char, String, int, int, RequestQueue) | 0 | 1 | 1 | 1 |
Elevator.buildingLook() | 7 | 2 | 5 | 7 |
Elevator.buildingStop(ArrayList<PersonRequest>) | 9 | 1 | 5 | 6 |
Elevator.close() | 4 | 1 | 4 | 4 |
Elevator.elevatorSleep() | 5 | 1 | 5 | 5 |
Elevator.floorLook() | 7 | 2 | 5 | 7 |
Elevator.getFloorDirection(PersonRequest) | 2 | 1 | 1 | 3 |
Elevator.getFloorIn(ArrayList<PersonRequest>, ArrayList<PersonRequest>) | 19 | 3 | 6 | 10 |
Elevator.getPersonIn(ArrayList<PersonRequest>, ArrayList<PersonRequest>) | 26 | 6 | 8 | 12 |
Elevator.hasFloorNext(ArrayList<PersonRequest>) | 4 | 1 | 4 | 4 |
Elevator.open(ArrayList<PersonRequest>, ArrayList<PersonRequest>) | 3 | 1 | 4 | 4 |
Elevator.run() | 25 | 3 | 9 | 11 |
Elevator.setDir(PersonRequest, ArrayList<PersonRequest>) | 17 | 1 | 9 | 12 |
InputHandler.InputHandler(RequestQueue, HashMap<Character, RequestQueue>, ArrayList<RequestQueue>) | 0 | 1 | 1 | 1 |
InputHandler.run() | 11 | 3 | 7 | 7 |
MainClass.main(String[]) | 2 | 1 | 3 | 3 |
OutPut.println(String) | 0 | 1 | 1 | 1 |
RequestQueue.RequestQueue(Boolean) | 0 | 1 | 1 | 1 |
RequestQueue.addRequest(PersonRequest) | 1 | 1 | 2 | 2 |
RequestQueue.getOneRequest(int) | 5 | 2 | 4 | 5 |
RequestQueue.getPersonRequests() | 0 | 1 | 1 | 1 |
RequestQueue.isEmpty() | 0 | 1 | 1 | 1 |
RequestQueue.isEnd() | 0 | 1 | 1 | 1 |
RequestQueue.setEnd(boolean) | 0 | 1 | 1 | 1 |
compare(PersonRequest, PersonRequest) | 0 | n/a | n/a | n/a |
Class | OCavg | OCmax | WMC | |
Controller | 4 | 7 | 8 | |
Elevator | 5.15 | 10 | 67 | |
InputHandler | 3.5 | 6 | 7 | |
MainClass | 3 | 3 | 3 | |
OutPut | 1 | 1 | 1 | |
RequestQueue | 1.38 | 3 | 11 |
本次作业中由于又优化了电梯调度策略,调度方法的复杂度显著升高,甚至有些方法由于多次使用for循环以及if else分支,复杂度达到了极高。从某种角度而言,代码复杂度有些过高。但经过本人尝试,相较于电梯调度以及漏人、空跑等造成的额外时间,代码的运行显著节约了开销,因此牺牲一部分算力,来换取更高效的调度策略并不失为一种可行的办法。
(4)Bug分析
-
本次作业可以说是非常惨烈,优化了两天后我觉得性能分会爆表,事实上确实运行速度很快大概99.7~8的性能分数,但是大意失荆州,由于不当notify,我轮询了,最后只过了一个点,甚至没有进互测(
没错反向爆炸了)。但在更改过程中我又遇到了其他问题,在几个小时的搏斗以后,我更深刻的理解了notify的使用,也是由于参考了官方实验的架构,前两次作业过于顺利,然后也并没有真正深刻的理解多线程的线程安全设计,导致了强测的大型翻车现场。
3. 第七次作业
(1)作业要求
-
本次作业在前两次满足纵向及横向请求的基础上增加了换乘的请求,注意横向请求由于不一定存在横向电梯可能也需要换乘,整体而言架构上仍可继续保留,需要考虑一下换乘策略以及线程终止的方法。此外增加了电梯的可定制化,如速度,容量以及可达性,其中可达性需要额外处理,其余均在构造时传入即可。
(2)具体实现
-
换乘策略实现:
首先,在输入线程中处理请求,判断是否需要换乘,并重新实现PersonRequest类,增加变量来存储换乘信息。关于换乘策略,主要有两种,一种是基于官方策略展开,即只换乘一次,仅需存储一个中间楼层,输入时静态处理请求;另一种是使用最短路算法,每次增加电梯时更新代价,在运行过程中对请求动态拆分,可以找到多次换乘的路径。在实现时我也参考了其他同学的做法,一部分图算法仅考虑了电梯的运行时间而并未考虑等待时间,实际上多一次纵向的换乘等待时间可能大大增加,因此不考虑等待时间的策略是不全面的,甚至性能上可能出现负优化。由于上周的教训,最终我实现了基准策略,并且尽量少纵向换乘。
-
线程结束方法:
本次作业中线程结束是一个难题,因为我们之前的结束标志是输入终止并且请求队列以及电梯中没人就结束,事实上由于换乘请求的增加可能出现这样的情况,仅有一个电梯中有乘客并且输入已经终止,这时按照以往的判断其他电梯会被杀死,但这一乘客可能需要换乘,使用其他电梯,因此这种结束方式需要修改。
我的实现方法是在总体队列中设置一个计数器,输入请求时检测需要拆分成几个请求,用计数器记录,在电梯中下人时代表完成一个请求,计数器减一,当计数器为0时就可以终止线程,杀死所有电梯。
-
轮询检查:
本次作业中极易出现轮询现象,在此提供一种检查方法,可以在每个线程while ture的第一行输出一行信息,在sleep以及notify时输出信息,这样便可以看出是否线程被频繁唤醒。
(3)代码复杂度分析
Method | CogC | ev(G) | iv(G) | v(G) |
---|---|---|---|---|
Controller.Controller(RequestQueue, HashMap<Character, RequestQueue>, ArrayList<RequestQueue>) | 0 | 1 | 1 | 1 |
Controller.run() | 31 | 6 | 13 | 14 |
Elevator.Elevator(char, String, int, int, int, double, int, ...) | 0 | 1 | 1 | 1 |
Elevator.buildingLook() | 7 | 2 | 5 | 7 |
Elevator.buildingStop(ArrayList<Person>) | 9 | 1 | 5 | 6 |
Elevator.close() | 4 | 1 | 4 | 4 |
Elevator.elevatorSleep() | 15 | 1 | 9 | 9 |
Elevator.floorLook() | 8 | 3 | 5 | 8 |
Elevator.getFloorDirection(Person) | 2 | 1 | 1 | 3 |
Elevator.getFloorIn(ArrayList<Person>, ArrayList<Person>) | 21 | 4 | 6 | 11 |
Elevator.getMask() | 0 | 1 | 1 | 1 |
Elevator.getPersonIn(ArrayList<Person>, ArrayList<Person>) | 26 | 6 | 8 | 12 |
Elevator.hasFloorNext(ArrayList<Person>) | 6 | 3 | 4 | 5 |
Elevator.judgeFloor(Person) | 1 | 2 | 1 | 2 |
Elevator.judgeFloorSleep(ArrayList<Person>) | 4 | 3 | 3 | 3 |
Elevator.open(ArrayList<Person>, ArrayList<Person>) | 12 | 1 | 7 | 7 |
Elevator.run() | 25 | 3 | 9 | 11 |
Elevator.setDir(Person, ArrayList<Person>) | 17 | 1 | 9 | 12 |
InputHandler.InputHandler(RequestQueue, HashMap<Character, RequestQueue>, ArrayList<RequestQueue>, ArrayList<ArrayList<Elevator>>) | 0 | 1 | 1 | 1 |
InputHandler.needTrans(PersonRequest, int) | 4 | 4 | 3 | 4 |
InputHandler.nextPos(PersonRequest) | 19 | 2 | 5 | 9 |
InputHandler.run() | 23 | 3 | 10 | 11 |
MainClass.main(String[]) | 3 | 1 | 4 | 4 |
OutPut.println(String) | 0 | 1 | 1 | 1 |
Person.Person(PersonRequest, int, boolean) | 4 | 1 | 3 | 3 |
Person.getFromBuilding() | 0 | 1 | 1 | 1 |
Person.getFromFloor() | 0 | 1 | 1 | 1 |
Person.getNextPos() | 0 | 1 | 1 | 1 |
Person.getPersonId() | 0 | 1 | 1 | 1 |
Person.getPersonRequest() | 0 | 1 | 1 | 1 |
Person.getToBuilding() | 0 | 1 | 1 | 1 |
Person.getToFloor() | 0 | 1 | 1 | 1 |
Person.isNeedTrans() | 0 | 1 | 1 | 1 |
RequestQueue.RequestQueue(Boolean) | 0 | 1 | 1 | 1 |
RequestQueue.addCnt(int) | 0 | 1 | 1 | 1 |
RequestQueue.addRequest(Person) | 1 | 1 | 2 | 2 |
RequestQueue.getCnt() | 0 | 1 | 1 | 1 |
RequestQueue.getOneRequest(int) | 1 | 2 | 1 | 2 |
RequestQueue.getPersonRequests() | 0 | 1 | 1 | 1 |
RequestQueue.isEmpty() | 0 | 1 | 1 | 1 |
RequestQueue.isEnd() | 0 | 1 | 1 | 1 |
RequestQueue.outPerson() | 1 | 1 | 2 | 2 |
RequestQueue.setEnd(boolean) | 0 | 1 | 1 | 1 |
compare(Person, Person) | 0 | n/a | n/a | n/a |
Class | OCavg | OCmax | WMC | |
Controller | 5 | 9 | 10 | |
Elevator | 5.06 | 10 | 81 | |
InputHandler | 5.25 | 9 | 21 | |
MainClass | 4 | 4 | 4 | |
OutPut | 1 | 1 | 1 | |
Person | 1.22 | 3 | 11 | |
RequestQueue | 1.27 | 2 | 14 |
整体复杂度和上次作业保持相近,由于更多策略的引入,因此部分run方法的复杂度继续走高。
(4)Bug 分析 & Hack策略
emmm,在一刀6杀的那一刻我就意识到了事情的严重性,强测又一次寄了,仔细检查了有没有轮询,结果不知道什么时候手残多打了个return,导致某些电梯卡在不可达层不再运行,而且不停while(true)最终轮询。随机数据轰炸了几千组但并没有注意初始就把电梯放在不可达层的情况,酿成了另一桩惨案。互测只能说,我的bug不会被hack到,我丢的🔪倒是意外的斩获了很多意想不到的bug。
本单元中hack主要提交了一些卡时间边缘丢入大量请求的数据,一方面请求比较密集,更容易触发线程安全问题,另一方面可以更好的尝试能否卡出TLE。
三、单元总结