BUAA OO 第二单元 电梯
文章目录
一、同步块的设置和锁的选择
在Homework_5中,我将同步块和锁设置在了PassengerQueue
类中,每个电梯线程都有这个类的两个对象,分别是persons
和processQueue
,代表此时在电梯中的人和即将进入该电梯的人(之后称它为等待队列)。因为每个电梯线程都有各自的等待队列,所以电梯之间的线程交互会减少,这样的设计的好处是让我在最初的作业中并没有出现线程不安全等问题(当时确实也没有线程安全这个概念)。所以我只在PassengerQueue
类中的方法设置了synchronized
,然后就比较顺利地通过了这次作业。
在Homework_6中,要求增量开发增加电梯和维护电梯的功能。在完成作业的过程中,我发现按照Homework_5的架构,我可以直接通过对elevators
的add
和remove
操作实现本次作业。因此本次作业中我并未进行同步块和锁的相关操作,就很顺利地通过了Homework_6。
在Homework_7中,需要实现对单个楼层服务中的电梯数进行限制。受到Experiment_4的启发,我采用了Semaphore
来实现,新建了Floor
类,通过acquire
和release
方法来进行锁的操作。
二、作业代码分析
1. Homework_5
1.1 UML类图
1.2 UML协作图
2. Homework_6
2.1 UML类图
2.2 UML协作图
3. Homework_7
3.1 UML类图
3.2 UML时序图
4. 结构分析
(下为Homework_7代码分析结果)
- Class metrics
class | OCavg | OCmax | WMC |
---|---|---|---|
Elevator | 2.5 | 15.0 | 60.0 |
Floor | 1.0 | 1.0 | 3.0 |
InputThread | 3.6666666666666665 | 8.0 | 11.0 |
MainClass | 3.0 | 3.0 | 3.0 |
Output | 1.0 | 1.0 | 6.0 |
Passenger | 1.6666666666666667 | 3.0 | 10.0 |
PassengerQueue | 1.7777777777777777 | 6.0 | 16.0 |
Strategy | 3.7777777777777777 | 14.0 | 34.0 |
Total | 143.0 | ||
Average | 2.3442622950819674 | 6.375 | 17.875 |
- Method metrics
method | CogC | ev(G) | iv(G) | v(G) |
---|---|---|---|---|
Strategy.wakeUpAll() | 1.0 | 1.0 | 2.0 | 2.0 |
Strategy.Strategy(ArrayList) | 0.0 | 1.0 | 1.0 | 1.0 |
Strategy.setEndSign() | 0.0 | 1.0 | 1.0 | 1.0 |
Strategy.selectElevator(Passenger) | 14.0 | 4.0 | 7.0 | 10.0 |
Strategy.removeElevator(int) | 3.0 | 3.0 | 3.0 | 3.0 |
Strategy.judgeFinish() | 3.0 | 3.0 | 2.0 | 3.0 |
Strategy.getMinDist(Elevator, Passenger) | 26.0 | 6.0 | 10.0 | 16.0 |
Strategy.getEndSign() | 0.0 | 1.0 | 1.0 | 1.0 |
Strategy.getElevators() | 0.0 | 1.0 | 1.0 | 1.0 |
PassengerQueue.setEndSign(boolean) | 0.0 | 1.0 | 1.0 | 1.0 |
PassengerQueue.pauseElevator() | 1.0 | 1.0 | 2.0 | 2.0 |
PassengerQueue.PassengerQueue() | 0.0 | 1.0 | 1.0 | 1.0 |
PassengerQueue.judgeIn(int, int) | 4.0 | 3.0 | 3.0 | 4.0 |
PassengerQueue.isEndSign() | 0.0 | 1.0 | 1.0 | 1.0 |
PassengerQueue.isEmpty() | 0.0 | 1.0 | 1.0 | 1.0 |
PassengerQueue.getPassengers() | 0.0 | 1.0 | 1.0 | 1.0 |
PassengerQueue.detectFront(int, int) | 12.0 | 6.0 | 4.0 | 6.0 |
PassengerQueue.addRequest(Passenger) | 0.0 | 1.0 | 1.0 | 1.0 |
Passenger.setFrom(int) | 3.0 | 1.0 | 1.0 | 3.0 |
Passenger.Passenger(int, int, int) | 3.0 | 1.0 | 1.0 | 3.0 |
Passenger.getId() | 0.0 | 1.0 | 1.0 | 1.0 |
Passenger.getFrom() | 0.0 | 1.0 | 1.0 | 1.0 |
Passenger.getDirection() | 0.0 | 1.0 | 1.0 | 1.0 |
Passenger.getDestination() | 0.0 | 1.0 | 1.0 | 1.0 |
Output.out(int, int, int) | 0.0 | 1.0 | 1.0 | 1.0 |
Output.open(int, int) | 0.0 | 1.0 | 1.0 | 1.0 |
Output.maintainAble(int) | 0.0 | 1.0 | 1.0 | 1.0 |
Output.in(int, int, int) | 0.0 | 1.0 | 1.0 | 1.0 |
Output.close(int, int) | 0.0 | 1.0 | 1.0 | 1.0 |
Output.arrive(int, int) | 0.0 | 1.0 | 1.0 | 1.0 |
MainClass.main(String[]) | 2.0 | 1.0 | 3.0 | 3.0 |
InputThread.setEnd() | 1.0 | 1.0 | 2.0 | 2.0 |
InputThread.run() | 22.0 | 8.0 | 8.0 | 10.0 |
InputThread.InputThread(ArrayList, ArrayList) | 0.0 | 1.0 | 1.0 | 1.0 |
Floor.releaseJustGetInElvtNum() | 0.0 | 1.0 | 1.0 | 1.0 |
Floor.Floor() | 0.0 | 1.0 | 1.0 | 1.0 |
Floor.acquireJustGetInElvtNum() | 1.0 | 1.0 | 2.0 | 2.0 |
Elevator.setMaintain() | 0.0 | 1.0 | 1.0 | 1.0 |
Elevator.run() | 35.0 | 6.0 | 13.0 | 19.0 |
Elevator.personOutAll() | 1.0 | 1.0 | 2.0 | 2.0 |
Elevator.personOut() | 3.0 | 1.0 | 3.0 | 3.0 |
Elevator.personIn() | 12.0 | 5.0 | 5.0 | 6.0 |
Elevator.maintainElevator() | 4.0 | 2.0 | 5.0 | 6.0 |
Elevator.mainRequest() | 3.0 | 3.0 | 3.0 | 4.0 |
Elevator.judgeTurn() | 2.0 | 2.0 | 3.0 | 4.0 |
Elevator.judgeOut() | 3.0 | 3.0 | 2.0 | 3.0 |
Elevator.judgeMoveOrNot() | 0.0 | 1.0 | 1.0 | 1.0 |
Elevator.judgeIn() | 0.0 | 1.0 | 1.0 | 1.0 |
Elevator.judgeFloorAccess(int) | 1.0 | 2.0 | 1.0 | 2.0 |
Elevator.getProcessQueue() | 0.0 | 1.0 | 1.0 | 1.0 |
Elevator.getPersons() | 0.0 | 1.0 | 1.0 | 1.0 |
Elevator.getNowFloor() | 0.0 | 1.0 | 1.0 | 1.0 |
Elevator.getMaintain() | 0.0 | 1.0 | 1.0 | 1.0 |
Elevator.getElevatorId() | 0.0 | 1.0 | 1.0 | 1.0 |
Elevator.getDirection() | 0.0 | 1.0 | 1.0 | 1.0 |
Elevator.getAllPersonNum() | 0.0 | 1.0 | 1.0 | 1.0 |
Elevator.elevatorMove() | 2.0 | 1.0 | 1.0 | 2.0 |
Elevator.Elevator(PassengerQueue, int, double, int, int, int, ArrayList) | 0.0 | 1.0 | 1.0 | 1.0 |
Elevator.doorOperationSleep(int) | 1.0 | 1.0 | 1.0 | 2.0 |
Elevator.dodo() | 3.0 | 1.0 | 3.0 | 3.0 |
Elevator.dealAccess() | 4.0 | 1.0 | 1.0 | 4.0 |
Total | 170.0 | 103.0 | 127.0 | 163.0 |
Average | 2.7868852459016393 | 1.6885245901639345 | 2.081967213114754 | 2.6721311475409837 |
三、调度器设计与调度策略
在本单元的三次作业中,我的调度器均为Strategy
类,在此基础上实现了静态的调度,即每当InputThread
线程接收到新的request
时,就通过调度器将其分配给某一个电梯。因为我将调度器设计为了一个类,而非线程,这样的设计让我在完成作业的过程中可以非常顺利的解决调度器和电梯线程之间的交互问题,在调度时不需要进行锁相关操作。
在设计Strategy
类时,我采用了影子电梯的思路,按照ALS策略通过模拟电梯运行计算各电梯可以接到这个乘客的最短时间进行筛选。每个电梯有两个队列,在模拟电梯运行时,先将在电梯中的人运送至各自的目标楼层,在这个过程中,如果电梯经过了新的request
的起始楼层,就将此时的模拟时间记为该电梯接到该乘客的时间,若将在电梯中的人都送达后仍未经过新的request
的起始楼层,则将等待队列的乘客作为主请求继续运行,直至可稍带,或等待队列也被清空后直接将该请求作为主请求,令电梯运行至该请求的起始楼层后,返回模拟时间。
在此基础上,再按照人数平衡的原则,将模拟时间相同的情况下,将新的请求尽可能分配给人数较少的电梯。
综上所述,通过这种影子电梯的调度策略,可以很好的实现运行时间方面的性能。不过在耗电量方面,我在设计之初并未将其作为主要因素考虑,但在完成作业后再次思考我的调度策略,首先可以发现这种调度策略下,每个新的请求只会引起一个电梯的移动,相较自由竞争的调度策略而言在耗电量方面有明显的优势,并且影子电梯调度策略的耗电量其实并不大,如果希望进一步优化耗电量,那么可以在模拟时间相同和人数平衡时,再选择其中耗电量最短的路径。
四、迭代过程中的扩展和改变
在本单元的迭代过程中,三次作业均以MainClass
主线程、InputThread
输入线程和若干个Elevator
电梯线程为主要结构,通过输入线程调用Strategy
调度,最后在各电梯线程中完成相应操作后通过Output
静态类完成输出。在此结构的基础上完成增量开发。
- 可扩展性:首先正如上文提到的
Strategy
和Elevator
之间的关系,因此在迭代开发中,对于电梯的调度有较大的扩展性,这一点在两次的作业迭代中体现十分明显。在Homework_6中,只需增加对电梯Maintain
状态的判断即可。在Homework_7中,可直接增加针对不可直达时的路径查找的算法即可。 - 三次作业中稳定的内容:我认为这三次作业中存在的最稳定的内容是上述的代码的基本架构是稳定的,所有增量开发都是在此架构上实现的。
- 三次作业中易变的内容:在迭代过程中改变最多的首先是输入线程
InputThread
,因为其要不断更新实现能够识别新的指令类别。其次是调度器Strategy
,在两次增量开发中其内容均产生变化,因为电梯的情况和状态会不断增多,所以调度器要能将每一种情况考虑在内。
五、bug与多线程debug
分析典型bug
- 首先分析我们在之前的代码设计中会遇到的普通bug,不过在这个单元,每个bug都会与多线程有着千丝万缕的联系,这其实也是这个单元bug出现的可能性增大的一个原因。比如,我在实现
Strategy
类时,因为最初对多线程并不熟悉,想用synchronized
尽可能地将许多方法加锁,但却导致了死锁等问题。 - 此外当线程交互时也易产生bug。比如,在实现Homework_6时,如果输入的最后一条指令为
maintain
,那么之后输入线程结束,处于暂停状态的电梯会结束线程,但被维护的电梯此时需要向其他电梯输入乘客,这就会造成电梯丢失乘客的情况。
多线程debug
首先是可以使用Jprofiler和Jconsole等软件辅助,其次是通过自己构造数据或者使用评论区里的评测机寻找潜在的bug。当然,再多的数据测试都不能证明程序的正确性,当代码出现未知bug后最重要的还是复现代码运行逻辑(能做到是最好的了 只不过真的好难)。
六、心得体会
这个单元终于是走完了三次作业的漫漫长路,在这个过程中我也从最初对线程、锁等概念一无所知,转为了能完成一个多线程的电梯系统。虽然初次接触多线程,实现代码时无从下手、debug时一脸茫然,但这三个周确实令我收获良多,每次作业中遇到的新问题都能让我get到新的知识点。
回顾本单元,线程安全是最主要的问题,在学会wait、notifyAll的方法之后,结合synchronized,可以初步完成Homework_5。之后进一步学习到的读写锁等更加灵活的方法,从而更简洁的实现作业功能。最后又接触到semaphore,通过acquire和release方法来对信号量进行管理。在这个过程中我体会到了每种方式都有着其独特的作用和实用场景。在设计代码时要针对架构理清各个线程的逻辑,选用合适的锁、在正确的地方加锁,才能防止死锁、轮询等问题,实现线程安全。
在本单元中我充分感受到了层次化设计的优点,完成了OO两个单元的学习后,我感觉自己对面向对象的思想有了一定的了解,在我目前的角度来看每次要完成的代码,实际上首先我们要对待实现的功能主体有充分的认识,而后选择合适的模型或者说架构,层次化设计在这里起着十分重要的作用,只有分清各层次,才能更好的在代码中实现高内聚低耦合,但也不能过于执着地进行分层,否则会使代码变得过于抽象繁琐,进行很多无用的设计。
在最后,希望能做到“关关难过关关过,前路漫漫亦灿灿”。