OO第二单元总结
多线程实时电梯系统---第一次作业
架构
InputThread
处理输入的线程
当收到的personrequest是null时,代表用户已经输入了^D,此时将五座大楼的等待序列设置finished并通知每座大楼,唤醒正在wait的线程。
若收到的request不是null,将会找到对应building的request然后加入,同样在最后唤醒线程。
Request
主要存的是HashMap<Integer,HashSet<Person>> waitingList,其中key是等待者所在的楼层,HashSet
add:在等待者集合中添加等待者 在InputThread中输入新的request的时候被调用
setFinished:Input结束时调用
Controller
每一台电梯均有对应的Elevator,控制该电梯的行为
stop:
得出电梯目前是否应该停止
有下:乘客中有要下电梯的---看是否有key为当前楼层的
有上:若等待的有在当前楼层的且电梯下人后有空位
有下或者有上则停下
getOn:
得出在当前状态下上电梯的人都有谁
先从电梯对应的waiting里面寻找是否有在当前楼层等待的人
若电梯为空:遍历并得出请求最多的方向 将该方向设为电梯方向并带上往这方向的人
若电梯不为空,带上同向最快到达的人
getOff:
得出当前状态下电梯的人都有谁
遍历乘客 如果有key为当前楼层的乘客就放到off里面
setDir:
确定方向
上下扫描观察是否有人在等电梯
若电梯为空:
上下均无人:电梯等待
上有下无:电梯上升
上无下有:电梯下降
上下均有:电梯往最远的走
到达顶层/底层:
掉头
其他:
保持原方向
Elevator
运行:
若电梯里没人,没人在等这部电梯:(锁死)
输入结束了,终止运行
输入没结束,wait
锁死过程:
如果需要停止,则停止并且进行上下人操作
判断此刻是否终止/等新需求
若不需要终止/等 则设置方向并且移动
多线程实时电梯系统---第二次作业
本次作业比起上一次作业,可以动态增加电梯,还可以增加环形的电梯使其能够在不同的楼宇之间移动。
在这次作业中考虑使用显式的lock而不是上一次作业的synchronized来进行线程之间的共享数据的保护。
调度器分析
每一部电梯都有其对应的队列,通过调度器寻找合适的队列将person插入该队列,电梯只从自己对应的队列取请求。
Dispatcher对每一个人进行一个权重计算,把所有等待的人离这个人的距离以及电梯内乘客离这个人的距离加起来(乘客的距离要加一,保证计算入同一层乘客),选择权重最小的队列。
想法是这样可以平均分配请求,让人选择离自己较近、较少人的电梯。
但这个方式让本电梯系统龟速前进,不如不要。
线程安全分析
对线程内共享的数据进行分析:
Building/Floor
waiting:(在该层/栋等待的人)
InputThread:
通过调用类中的addPerson对waiting内的人进行增加及分配
-----addPerson通过writelock锁住对象
通过调用类中的addEle用dispatcher方法对waiting内的人进行分配
-----addEle通过writelock锁住对象
Dispatcher: allocate方法将waiting内的人去除,分配至对应的requestqueue -----每次调用allocate的时候都已经获得了writelock 不会冲突
elevators:(在这层/栋的电梯)
Elevator
requestQueue:(等待上这部电梯的人)
InputThread:
finished:
输入结束的时候会对requestQueue进行setFinished
-----notifyAll(唤醒在电梯里被设置成wait的线程)
waiting:
调用的Dispatcher方法会对requestQueue内容进行读取
调用的Dispatcher方法会对requestQueue人进行增加
-----
Elevator:
finished:
判断是否停止的时候会读取
waiting:
确认是否停止/上的人数时会对requestQueue内容进行读取
上人的时候会对requestQueue进行修改
-----requestQueue中每个方法均用了synchronized,不会出现不同线程同时使用其中的方法情况
-----所有通过get得到其址的方法进行计算的时候都会把值放入另一个容器
Controller: stop&hasOn均通过读取电梯中的requestQueue判断是否结束 getOn通过获取其内容找到需要上电梯的是哪些人 -----三种方法均通过getRequestQueue把requestQueue读出 -----方法上了读锁 -----只有该Elevator线程可访问requestQueue内的waiting 不会冲突
passengers:(这部电梯上的人)
InputThread:
其Dispatcher会通过读passenger中的内容对其访问
-----读锁
Elevator:
其checkstop等函数会通过读passenger中内容对其访问(不冲突 因为只在本线程写了)
其gap过程上下人会改变passenger内容
-----读锁&写锁
多线程实时电梯系统---第三次作业
这次吸收了上一次的教训 决定返璞归真
扔掉调度器 开摆
由于牵涉到换乘,在做决策时需要对其他楼层/楼的电梯情况进行读取,如果继续用之前的数组的模式要不停的传参,很麻烦。在这里对楼层集合以及楼集合都采用了单件模式,可以更方便的读取。
线程安全分析
由于采用集中调度(自由竞争)的方式,需要维护的线程间共享资源一下子减少了很多,只有在每层楼和每栋楼等待的waiting这个HashMap以及Elevator中的isFinished变量。
其中isFinished变量采用AtomicBoolean,可以确保其线程安全。
InputThread
ElevatorRequest
在新建电梯的时候进行了传址操作,把waiting传入电梯,不会造成冲突
PersonRequest
在Building/Floor中进行addPerson操作,会对waiting进行修改,用synchronized锁住并在操作完成后进行notifyall唤醒线程
Controller
hasOn,stop,setDir均对waiting进行了读取,用synchronized锁住
getOn也对waiting进行了读取 并且从waiting中得到了HashSet
getOn的产生结果on的使用在synchronized块中 不会发生冲突
Elevator
checkstop对waiting进行了读取,采用synchronized锁住,若无请求则用wait释放该锁
setFinished会对waiting.wait()的线程进行唤醒
gap内对waiting进行了很多读取,修改的操作
对于换乘层/栋的读取操作(nextwaiting)
用synchronized锁住
将该person加入nextwaiting并唤醒等待该waiting的线程
对于其他所有的waiting操作
用synchronized锁住
假如该person已到达最终目的地则唤醒这些线程看是否最终结束
对本电梯waiting操作
synchronized锁住再进行移除
BUG分析
第一次作业
由于记错了清明放假的时间,以为一周后交作业,本地修好的版本一直忘记交,导致最后强测开始了才发现是最初的版本。
最初的版本有明显的bug,就是在计算上电梯的人的方法里有对电梯人数进行直接操作的步骤,而该方法被调用多次,导致电梯内人数非常诡异。在本地简单的手动构造一组数据即可看出,但遗憾的是忘记交了,导致强测因此只有42分。
第二次作业
强测没有bug,但由于调度策略有问题,性能分很低,去了B房
第三次作业
虽然用了自由竞争,但是电梯还是很慢,导致RTLE了一个点,本地跑了一下发现差了1s,简单的把开关门期间下人的一行代码放在了sleep之前,让更多换乘的电梯早一点被唤醒,即可达到时间要求。
三次互测都采用黑盒测试,大部分找到的别人的bug都是线程不安全或者上下人数出了问题。
类图展示
由于作业为迭代开发,在此展示第三次作业类图
线程协作图展示
心得体会
刚开始接触多线程,很多地方都云里雾里的。特别是在第二次作业,在加入了调度器后,很多对共享量的访问都不太安全,一开始没意识到对某些变量的传址访问其实是不安全的,没有放在加锁区域,在进行大量测试后才发现这样的问题。同时对锁的使用比如condition这些唤醒方法都不太熟练,导致写的时候很慢。
这个单元最大的遗憾就是第一次作业用的是最初的版本,忘记交本地的版本。假期在玩之前一定要注意群里的消息以及各种时间节点。但也希望课程组以后能不能稍微提高中测的难度,或者像计组一样增加一组不计入分数的测试点,让同学们能更好的完成作业。(或许当时如果我没过就不会把本地版本扔那忘记交了)
三次电梯的性能其实都很差,可能是对线程的变量访问过于频繁的问题。大道至简果然还是没错的。