2022-OO-Unit2

2022-OO-Unit2

一、架构分析

第一次作业

在写第一次作业时,我对多线程了解程度还不是很深,并且清明假期有点浪,导致最后一晚在赶ddl。所以第一次作业的架构基本借鉴了上机实验的代码(甚至变量名都没变)。

程序中的共享对象为RequestQueue,这是唯一的共享对象。我认为这是实验代码最有亮点的地方,它把程序中的共享对象封装为一个类,这样就可以无脑在这个共享对象类里加synchronized关键字了。

InputThread线程获取输入,得到请求队列waitQueue。之后Schedule线程获取waitQueue,将其分配给各个Elevator类对象。之后各个Elevator实例对象将Schedule分配来的请求进行运送。

第二次作业

第二次作业由于加入了水平电梯,并且扩大了电梯的数量。所以我在第一次作业的基础上进行了扩展(扩展力度近乎重构)。

第二次作业较第一次作业而言,保留了InputThread、Output与PersonRequestQueue类,添加了电梯请求队列类ElevatorRequestQueue类,重新修改了Schedule类,添加了ElevatorAdder类,添加了楼座类VerticalElevatorGroup类,添加了各个楼层类HorizontalElevatorGroup,并添加了垂直电梯VerticalElevator类与水平电梯HorizontalElevator类。

InputThread读取所有的输入请求,得到乘客请求队列personWaitQueue电梯请求队列elevatorWaitQueue,Schedule将personWaitQueue调度至各个楼座VerticalElevatorGroup和楼层HorizontalElevatorGroup,ElevatorAdder根据输入发出的elevatorWaitQueue将电新梯添加至相应楼层或楼座。之后各个楼层或楼座内的电梯自由竞争Schedule调度来的乘客请求,对其进行运输。

第三次作业

第三次作业沿用第二次作业的架构,改动很少。主要有以下三点。

  1. 第三次作业中乘客请求发生了变化,可以“斜着走”,所以必须重写Person类。
  2. 此外,在第三次作业中不能通过输入为null来判断请求队列的结束,因为乘客转乘会带来新的请求。即请求除了从输入中来,还可以从请求中来,也就是从电梯中来。所以我加入了Counter类,内置一个请求计数器。每从输入中得到一个请求,计数器加一;每当乘客到达最终目的地,计数器减一。当计数器为0时,表示所有乘客均到达目的地,可以结束所有线程了
  3. 在每个VerticalElevator中加入所有的HorizontalElevatorGroup,这样乘客在离开垂直电梯之后,如果还未到达目的地,可以继续将请求添加至对应的HorizontalElevatorGroup。同理,也需要在HorizontalElevator中加入所有的VerticalElevatorGroup,使得乘客离开水平电梯后,若未到达目的地,可以继续将请求添加至对应的HorizontalElevatorGroup。

二、锁的使用和同步块的选择

第一次作业

第一次作业的共享对象全部是由RequestQueue类的对象。所以只需要在RequestQueue类内的方法添加synchronized即可。在需要修改RequestQueue对象状态的方法中添加notifyAll()语句,用来通知别的线程“各位可以醒醒了,我这边状态更新好啦,你们可以尽情地读写我了”

谨记:如果对共享对象读而不写的话,只加synchronized,不需要加notifyAll(),如果加了可能会轮询。

第二次作业

第二次作业中的共享对象比第一次作业要复杂一些(因为我的架构更复杂了)。

共享对象分为三类,乘客请求队列电梯请求队列电梯

在这次作业中我使用了读写锁,即ReentrantReadWriteLock,通过调用读写锁,减少了synchronized关键字的使用。但是有一个问题,由于java语法限制,需要notifyAll()的地方必须加synchronized关键字,这样使得我最后还是在很多地方加了synchronized(捂脸)。

InputThread与Schedule、ElevatorAdder共同读写乘客请求队列personWaitQueue和电梯请求队列elevatorWaitQueue。ElevatorAdder和Schedule共享各个楼层楼座的Elevator对象,其中ElevatorAdder只写不读Schedule只读不写。Schedule和各个楼层楼座共同读写楼层楼座内的PersonRequesetQueue对象。

所以我在每个RequesetQueue类中加入两把读写锁,一把锁用于对isEnd成员的读写,另一把锁用于对ArrayList<Requeset>对象的读写。两把锁不能混用。当一些语句需要读时,调用lock.readLock.lock()与lock.readLock.unLock()方法将其锁住;当一些语句需要写时,调用lock.writeLock.lock()与lock.writeLock.unLock()方法将其锁住。

第三次作业

第三次作业在第二次作业基础上,添加了一个全局共享对象——计数器,即Counter类,其内置一个int型变量,用于记录当前程序还需处理的请求数量。采用单例模式创建,并在读写操作上添加synchronized关键字,保证线程安全。只有计数器为0时,各个电梯线程才能结束

三、调度器设计与分析

第一次作业

第一次作业的调度比较简单,只涉及schedule将请求分配给每个电梯的调度以及电梯内部调度算法。前者比较简单,将该请求加入对应楼座的请求队列即可;后者我第一次作业中使用了als算法,没什么新奇的点,便不再赘述。

第二次作业

第二次作业添加了横向请求。这就要求schedule在原有基础上,将横向请求添加至对应的横向电梯的对应层。并且由于每个楼层楼座由多部电梯,需要将每个楼层、楼座内的乘客调度给电梯,在这个地方我选择了自由竞争,因为这样最简单(捂脸)。

此外由于加入了横向电梯,需要设计横向电梯的调度算法。

我的横向电梯调度算法如下:没有乘客时,电梯不动。有乘客出现时,前往乘客所在的楼座。之后便仿照look算法,有正向乘客时电梯正向走,没有正向乘客时电梯反向走。

其中根据请求的起点和终点判断电梯运行方向的算法如下:令tmp为**(((destination - start) % 5) + 5) % 5**,如果**tmp % 5 != 3 && tmp % 5 != 4**,电梯正向走,否则,电梯反向走。这个表达式由人工智能专业的舍友在中国剩余定理的基础上提出,在此我不得不感到数学是真的强大。

第三次作业

第三次作业的调度完全采用指导书中的基准策略,没什么新奇的点,便不再赘述了。

四、程序bug

第一次作业

进出门顺序搞反

正常的电梯逻辑应该是先出后进,但我在第一次作业中写成先进后出了,inDoor()和outDoor()方法顺序写反了,导致强测wa了一片,让我悔不当初,其实也怪我自己没有好好观察输出,没发现电梯开关门期间会超载。

安全输出类

输出线程的安全问题最初真的没有考虑到。在讨论区学习后,才意识到这个问题。具体做法:建立Output类,将官方输出作为静态成员变量,并加上synchronized关键字,保证程序在任一时刻仅有一个官方输出类在输出,保证了输出的时间戳为递增的。

第二次作业

电梯反复横跳

在第二次的循环电梯中,由于对各种上下电梯情形考虑不周,导致电梯没能在该停的地方停下,乘客没能在该进电梯的时候进去。不过这种bug是可以复现的。学了多线程,我才明白:可以复现的bug都不叫bug!

第三次作业

中转横向电梯的判断

在第三次作业中,我有一些点超时。我最初以为是死锁,但经过Jprofiler的分析后,发现根本没有产生死锁。是因为我把请求放到了错误的中转楼层,导致乘客进不了电梯,从而超时。

正确的做法是看该楼层是否存在一部电梯,该电梯可以在起点楼座和终点楼座停车,但我错误地写成了判断该层是否存在一部电梯,该电梯可以在起点楼座停车 && 是否存在一部电梯,该电梯可以在终点楼座停车。。。。

错的很彻底,血的教训。

五、互测

第一次作业

由于清明假期,第一次作业的互测时间调整到了4月6日。那天是我返校的日期,一整天都比较忙。所以没有参加互测(捂脸),所以不再多说了。

第二次作业

第二次互测中,我构造了一些数据点,包括“在短时间内输入大量请求”测试输入和一级调度器的稳定性,“短时间内在同一楼座输入大量请求”测试各个楼座二级调度的稳定性

第三次作业

第三次互测当天由于参与学院组织的活动,没有在互测上下很大功夫。只提交了两个随机生成的数据点。

六、心得体会

这一单元的作业难度之前早有耳闻,电梯惊魂夜等词语也听了好多。确实这一单元的作业难度很大,但我认为有了实验代码的帮助,在初次编写程序时并不是很困难。

线程安全

这一单元最难的在于对多线程的理解以及共享对象的选择、对共享对象的保护。这些其实说来简单,“只要这个对象共享了,并且有多个线程访问,至少一个线程对其进行了修改,就上锁”。至于上什么锁,可以使用synchronized关键字,也可以使用ReentrantLock,甚至是ReentrantReadWriteLock。其中使用后者的要求是要分辨清楚这个操作对共享对象是读还是写

层次化设计

共享对象的层次化很重要。在后两次作业中,我将共享对象分为两级,各个楼层和楼座共享输入请求队列每个楼层和楼座内的电梯共享本楼层楼座内的请求队列。这样设计可以保证较高的可扩展性,并且在改bug的时候也较容易一些。

虽然最终成绩不怎么样,但是在这一单元我学习到了很多知识,其中包括读写锁的使用、Runnable接口的使用等。最终学到知识才是最重要的!

posted @ 2022-05-03 10:15  yjzhao  阅读(58)  评论(1编辑  收藏  举报