一、设计策略
第五次作业
第五次作业是可捎带电梯,我采取的是生产者-消费者模式,由RequestInput线程读入请求,并将请求存入Request的List中,然后由Elevator线程通过judgeDirect方法确定电梯的运行方向。我采用的是SSTF调度策略,电梯每次到达一个楼层后寻找里当前楼层最近的请求,并设置对应的电梯运行方向。因为这次作业电梯没有容量限制,所以就遵循能进就进的策略。
对于线程之间的交互,采用RequestList这个共享对象,每次有新请求就存入RequestList,并提醒电梯线程,电梯线程就通过judgeDirect方法根据RequestList确定电梯运行方向,当电梯内为空且RequestLsit为空时,电梯线程就进入等待状态,直到有新请求到来或输入结束就将电梯线程唤醒。另外还设计了Manager类来对电梯的上下人进行管理。如前所述,因为这次作业没有电梯容量限制,所以对于乘客的管理就是能进就进,能出就出。
对于线程的退出我在Elevator类中定义了一个IsInput的静态布尔型变量,来判断RequestInput线程是否退出,如果RequestInput线程已退出且RequestList为空,就退出Elevator线程。
第六次作业
第六次作业时多部可捎带电梯,这次作业我采用调度策略是“电梯抢人”方式,每部电梯自行根据RequestList寻找里当前楼层最近的请求。但这样可能会有多部电梯一同到达某个楼层,但其中一个电梯把人接完了,另一部电梯开门了却没接到人的情况,于是就新定义了一个Hashmap<Integer, Boolean>类型的共享变量isPick,里面存储的是每个楼层是否被某部电梯定为目标楼层的情况。当某个楼层被某部电梯选为目标楼层,那isPick中这个楼层的value就被设为false,其他电梯就知道这个楼层的请求已经有电梯去处理了,不会再将这个楼层设为目标楼层。
每部电梯的寻找目标楼层的算法还是采用SSTF算法,并对Elevator类中的judgeDirect方法进行了相应修改,增加了判断某个楼层是否已被其他电梯设为目标楼层,这样就不会出现电梯接不到人的情况。通过judgeDirect方法得到目标楼层和电梯运行方向,到达目标楼层再通过Manager类控制上下人。主要的改动就是judgeDirect方法和新增的IsPick机制,其他的都是一些多电梯的相应小改动。
第七次作业
第七次作业把电梯分为三类,三类电梯的容量、速度、可达楼层都不同,另外还可以动态新增电梯。因为这次作业三种电梯的可达楼层不同,所以就必须要有换乘策略。这次作业改动了Elevator类中的judgeDirect方法,判断目标楼层该电梯是否可达,如果不可达就更改目标楼层。还要判断目标楼层的请求是否可以通过该电梯换乘。还修改了Manager类中的control方法和新增了isNeedArrive与findClosestFloor方法,findClosestFloor方法查找当前请求在当前电梯哪一楼层进行换乘到另一电梯,isNeedArrive判断当前请求是否可通过当前电梯通过换乘到达目的楼层。至于单部电梯寻找目标楼层的策略依然是SSTF(虽然这次作业新增了乘客等待时间的性能判定,但经过测试SSTF算法还是相对于Look等算法时间较优)。
由于这次作业存在换乘,所以按照原来的退出线程方式就可能出问题,当RequestList为空且当前电梯为空时,电梯就会退出线程,但其他电梯内可能存在乘客需要换乘当前电梯到达目的楼层,就会导致错误。所以我在Manager类中新建了共享变量elevatorThread,只有当所有电梯内为空且RequestList为空时,线程才会退出。
二、第三次作业的可扩展性
以电梯第三次作业的现有架构再进行一些扩展不会很复杂,各个线程分工较为明确,每部电梯也都是自己“抢人”运输,电梯与电梯之间没有复杂的交互关系。
SOLID:
- Open Close Principle:对于方法实现的较好,每个方法分工明确,要扩展时增加相应方法或替换相应方法即可。对于类的实现没有使用接口和抽象类,对于开闭原则满足的较差。
- Liskov Substitution Principle:只用到了对于Thread的继承,这个继承对于该原则实现的较好。
- Interface Segregation Principle:没有用到接口。
- Dependency Inversion Principle:没有用到接口。
- Demeter Principle:输入线程与电梯线程只通过共享对象进行交互,相互作用较少。
- Composite Reuse Principle:用到的继承较少,聚合关系满足的较好。
三、基于度量的程序结构分析
因为这三次作业的迭代改动不多,结构变化不大,这里就只分析最后一次作业。
UML图:
复杂度:
主要分为RequestInput和Elevator两种线程,由RequestList读取请求并加入到RequestList中,电梯则根据RequestList通过SSTF算法决定运行方向与目标楼层,Manager则管理乘客的上下电梯,与换乘楼层的选择。
第七次作业复杂度飘红的地方较多,主要是Elevator的judgeDirect即寻找目标楼层的方法写的过于臃肿了,因为把“电梯抢人”的思想和SSTF的策略都集中到这个方法了,另外Manager类的管理乘客的方法也写复杂了,其实将这些方法要干的事情再细分一下,复杂度就不会飘红了,但当时写的时候没有意识到这点。另外对于run方法没有做到跟main一样简洁,在电梯第一次作业时因为第一次接触多线程,就没有管这么多,先追求做对。后面的两次作业一直没大改架构,导致Elevator的run方法越来越臃肿。
四、BUG分析
第一次作业没有发现BUG,第二、三次作业则在互测中被发现了bug,结果都是死锁的问题。第二次作业是在一个if语句与锁之间,一个电梯线程通过if语句判断是否满足退出线程的条件,不满足就进入锁,并wait,但是在if语句与锁之间其他线程的执行导致本来不满足的if语句满足了,所以导致本该退出的线程无法正常退出。第三次作业同样是一个很隐蔽的类似的问题,需要运行上百次才会出现。
五、互测策略
本次评测机的搭建分为三个部分,第一个部分是数据生成器,可在随机时间随机生成请求数据,第二个部分是正确性判断程序,根据指导书上的正确性判断将输入数据和电梯程序输出进行比较判定结果是否正确,第三部分是利用python的subprocess所写的程序,负责将数据生成器的输出实时输入到电梯程序,并调用正确性判断程序判断结果是否正确。
尽管评测机搭建的还可以,但三次作业却一个bug都没有hack到。第一次互测是大家都没有发现bug,第二次互测结束后发现别人hack用的数据都是导致程序不能正常终止的数据,而我在第二次互测中当运行评测机发现有时候评测机终止不了的情况,我还认为是我评测机的某些问题,这也导致了我没有发现第二次作业中死锁的bug。第三次互测后则发现那些hack成功的数据基本上是同一时间大量输入的,虽然我用评测机不间断测试了几百次,只考虑过缩短请求的间隔时间,却没考虑到同一时间大量输入的问题,导致没有发现bug。
这个单元的互测并不成功,主要原因还是在于太过依赖于评测机了,只是在后台挂起评测机不断评测,而没有手动构造或修改评测机构造各种极端数据。
六、心得体会
通过这三次作业对多线程有了相对清晰的了解,通过对线程安全也有了一些深刻的认知。一开始对于多线程不了解的时候,对于第一次作业如何构造很是头痛,后来在线程安全的问题也吃过亏。不过也学到了很多东西,对于多线程的设计也有了一些想法,也学会了多线程的调试。总的来说,经过这三次作业取得了一些进步,但也暴露了很多问题,比如很多方法复杂度较高,面向对象的很多原则并没有严格满足,还是有待提升啊。