编程语言
首页 > 编程语言> > Java多线程程序设计总结——电梯

Java多线程程序设计总结——电梯

作者:互联网

第一章 基本架构

第一次作业架构

二话不说,先上架构。

第一次作业架构

总体设计

总的来看,我的作业架构主要包括输入类(InputHandler),总调度器(Simulator),电梯类(Elevator),乘客类(Passenger),输出类(OutputHandler)。输入类不断将请求打包为乘客类后放入总调度器的等待队列中,总调度器从自己的等待队列中取出请求分配到合适的电梯等待队列中。电梯在合适的楼层从自己的等待队列中取出请求,放入自己的运行队列中。当电梯发出一定的行为时,电梯调用输出类输出。在我的架构中,输入类,调度器,电梯类均为线程,输出类不是线程。

在完成第一次作业我就本着第一次就把事情做对的态度,充分考虑了后续作业的扩展性。这主要体现在以下方面。

部件设计

输入设计

输入类是线程,实时接受外部输入,并调用乘客工厂的方法将请求包装为乘客对象,放到调度器的总等待队列中。

调度器设计

调度器采用单例模式,负责将总等待队列中的乘客分配到合适电梯的等待队列中,本身是一个线程。之所为调度器单开线程,是因为输入线程需要实时接受输入,如果在输入线程中进行乘客的分配难以满足输入的实时性需求。

调度器中包含两个队列,一是乘客的总等待队列,二是电梯队列。电梯队列单独开设一类ElevatorPool,采用单例模式,便于动态增加电梯。

第一次作业中调度器线程结束的条件是输入线程停止并且调度器等待队列为空。

电梯类设计

电梯类是这三次作业中代码量最大的类。电梯类除get,set方法外,主要包含了以下方法:

第一次作业中电梯的终止条件为:输入线程结束 && 调度器的等待队列为空 && 自己的等待队列为空 && 自己的内部队列为空。策略类在电梯刚到站点和电梯准备出发时均会检查电梯是否可以停止,如果是则调用电梯的end()方法,将电梯线程状态置为停止。

以下是第一次作业的时序图:

第一次作业时序图

从中可以看到,中间的并行块par中有三个并行的Loop块,它们分别代表InputHandler不断将乘客放入Simulator的等待队列中,Simulator不断将等待队列中的乘客分配到相应Elevator的等待队列中,Elevator运行时不断将到达目的地的乘客从运行队列中赶走,并且把自身等待队列中的乘客取出放入运行队列中这三个并行的过程。

此外,Simulator的停止条件为等待队列comingPassenger为空且输入线程结束,电梯的停止条件为输入线程结束 && 调度器的等待队列为空 && 自己的等待队列为空 && 自己的内部队列为空,这一条件可以简化为Simulator结束 && 自己的等待队列为空 && 自己的内部队列为空。这样,输入线程一定最先结束,调度器随后结束,各电梯也陆续结束。

为突出重点,我省略了OutputHandler的相关交互,相信聪明的读者可以自行理解。

策略设计

我采用Look策略,该策略的原始版本为电梯从最底层移动到最顶层,然后从最顶层移动到最底层。这一策略优化后的行为是:最高移动到max(等待队列中乘客出发楼层最高点,内部队列乘客到达楼层最高点),最低移动到min(等待队列中乘客出发楼层最低点,内部队列乘客达到楼层最低点),这样就需要频繁地地遍历等待队列域内部队列来更新目的地,因此被称为"Look"。

在我的Look策略中,电梯的初始状态为ArrivedAtPOint,在arrivedAtPointAct()方法中,首先判定是否结束(终止条件上文已经提到),如果到达则将电梯的isEnd标记为true,arrivedAtPointAct()方法返回,否则更新目的地(destination)与方向(direction),接着arrivedAtPointAct()方法调用电梯的wetherToOpen()方法判定是否开门,如果需要开门则将状态设置为OpenningStatus,然后调用电梯的doorOpen()方法。

在openingAct()方法中,首先调用电梯doorOpen()方法,将到达目的地乘客送走,等待0.2s,同时进人,开门结束根据电梯当前的乘客信息更新电梯目的地与方向,紧接着调用doorClose()方法,等待0.2s,同时进人。最后将电梯的状态设置为ReadToGo。

在readyToGoAct()方法中,首先判定电梯是否可以结束。如果可以结束则设置电梯的isEnd为true,方法返回,如果没有结束则更新电梯的目的地与方向,然后根据电梯的方向调用电梯的stepForward()或stepBack()方法。

第一次作业很遗憾我没有为Look策略进行更高层次的优化。我一开始的做法是只要电梯等待队列中的乘客所在楼层与电梯当前楼层相同则让乘客进入电梯,作业结束后我才发现可以在乘客目的楼层在电梯运动方向上时才让乘客进入电梯可以大大提升性能(夸张的数据点可以提高10s)。为此只需要在等待队列乘客进入电梯的判定条件中加上elevator.pointOnTheWay(passenger.getToPoint)。

输出设计

输出单开一个类,但是输出不是线程。输出类采用单例模式,其中用synchronized封装in, out, open ,close, arrive五个方法,实现输出线程安全。

第二次作业架构

第二次作业增加了环形横向电梯,动态增加电梯。其中横向电梯域动态增加电梯我已经预料到了,但是环形电梯是我没有考虑到的。

环形电梯的难点在于Look策略变了(当然也可以不要性能直接当一般电梯处理)。由于环形电梯总共有5个楼座可以到达,我的做法是对于环形电梯,将电梯当前位置前两个位置 - 电梯当前位置 - 电梯当前位置后两个位置视为电梯运行的路径,这样环形电梯就可以转化为一般电梯一样用Look策略规划路径。

一下是第二次作业架构图。

第二次作业架构

由于第二次作业需要在规划路径,前进后退时区分环形电梯与非环形电梯,我在电梯类中引入属性isCircular作为电梯是普通电梯还是环形电梯的标志。在findExtremePointInOneQueue(),findExtremePoint(),pointOneTheWay(),stepForward(),stepBack()方法中用if-else为普通电梯与环形电梯分别进行了实现。这样的坏处是电梯类代码膨胀,达到了400多行。

第二次作业允许一栋楼有多个电梯,这便产生了两种分配策略,一是自由竞争,即不再为每个电梯单独设置等待队列等着调度器去分配,二是在调度器中设置一个总的等待队列,电梯去里面"抢"人;二是调度器分配策略,即我再第一次作业中采用的策略。

强测结果表明自由竞争策略比调度器分配策略效率高5分左右。

第二次作业由于不涉及换乘,因此线程之间的协作关系与第一次作业类似,时序图就不上了。

第三次作业架构

第三次作业增加了有不可达楼层的环形电梯与乘客换乘的要求。

在第二次作业基础上,我在电梯池ElevatorPool中增加了为乘客规划路径的方法,该方法接受乘客作为参数,利用电梯信息为乘客规划一条路径,并将它填入乘客的路径数组中。为简单不易出错,我采用静态规划,即只在乘客到达之初规划一次路径,乘客到达中转站点时不重新规划路径。

我填充了调度器中passengerComeFromElevator()方法,该方法位于调度器中,仅会由电梯调用。该方法会判定乘客是否到达目的地,如果没有到达则更新乘客当前位置与下一个目的地,并将其放入调度器的等待队列中。

考虑到第二次作业电梯类因为if-else而膨胀到的400行代码是在不优雅,我采用了继承解决问题。对于电梯的共性方法,如get,set,doorOpen(),doorClose(),halt()等,我在抽象电梯类Elevator中实现它们,对于环形电梯与普通电梯的特性方法,如stepForward(),stepBack(),我分别在环形电梯类CircularElevator与纵向电梯类VerticalElevator中实现它们。最终我的抽象电梯类代码为290行,环形电梯类代码为280行,纵向电梯类代码为220行,虽然总数增多了,但单个类与单个方法的代码减少了,并且减少了if-else分支。

以下是第三次作业的架构。

第三次作业架构

图中电梯类看上去仍然很大,但这只是因为它的方法定义多,实际上电梯类有很多抽象方法是分别在环形电梯类与纵向电梯类里面分别实现的。

第三次作业涉及换乘,乘客不仅可以来自输入还可以来自电梯,因此电梯线程的终止条件不再是:输入线程结束 && 调度器的等待队列为空 && 自己的等待队列为空 && 自己的内部队列为空。因为即使这个条件满足,其他电梯中仍然可能存在换乘乘客需要使用当前电梯。

为简化实现,我规定所有电梯必须统一停止(前两次作业中电梯工作独立,因此可以轻松地做到电梯自己停止;第三次作业中电梯间建立了协作关系,因此某一个电梯终止的判定条件较为复杂且开销大),并且终止命令由调度器统一发出。为实现这一点,我参考了第四次课上实验第二个练习的做法,在调度器中定义了counter属性,该属性初始值为0,在乘客初次到达时加1,在乘客最终到达目的地时减1,在乘客到达中转站时不变。这样,输入线程结束 && counter == 0 便可以作为电梯与调度器结束的必要充分条件。

由于需要具备两个条件,因此满足结束条件有两种情况:

为实现第一种情况,我再Simulator的counterDecrease()方法中判定counter == 0 && 输入结束,若为真则调用finalEnd()方法;为实现第二种情况,我在Simulator类中的inputEnd()方法中判定counter是否为0,如果为0则调用finalEnd()方法。

finalEnd()方法调用了ElevatorPool的endAllElevator()方法,同时将Simulator的end标记为true,并且在synchronized(comingPassengers)中用comingPassengers.notifyAll()来唤醒可能正在等待的Simulator线程,从而达到结束Simulator线程的目的。

以下是第三次作业的协作图:

第三次作业时序图

可以看到,第三次作业电梯送出乘客后调用了Simulator的passengerComeFromElevator()方法,该方法根据乘客是否到达从而决定减小counter还是将乘客加入调度器等待队列中。同时,Simulator与电梯的停止条件也变为了各自的end属性为true。此外,Simulator还会调用finalEnd()方法结束自己与电梯线程。这样,输入线程仍然最先结束,调度器与所有电梯几乎同时结束。

第二章 同步块的设置

Java张主要存在两种类型的锁,一是synchronized同步块,它是Jvm级别的锁机制,可以将任一对象指定为锁,具有隐式释放锁的方便性二是lock,它是Java语言级别的锁,需要显示释放锁,比synchronized更灵活。

三次作业中我均采用synchronized()同步块,没有采用Java提供的lock锁。以最为复杂的第三次作业为例,synchronized主要用在以下对象上:

凡是使用上述对象的地方都应该处于同步代码块中!

第三章 程序bug

第一次强测与公测我均没有被测出bug。

第二次公测我没有被测出bug,但强测寄了一个点,错误是RTLE。具体原因是纵向电梯面对同时到达的一上一下两个乘客会出现上下横跳无法停止的情况,考虑以下输入:

[1.0]1-FROM-A-2-TO-A-1
[1.0]2-FROM-A-8-TO-A-9

如果1,2两个乘客被同时分配到电梯等待队列中,并且电梯此时位于2-8楼之间,则会出现上述情况,以电梯内部为空,位于6楼,处于关门状态并且direction = 1(向上)为例,具体分析如下。

出现这个问题的原因是电梯更新目的地发生在了乘客进入电梯前,原本的目的地设置在8楼就是为了让8楼乘客进电梯,现在更新了destination与方向后8楼的乘客却上不了电梯了。为解决这一问题,我规定电梯在到达某一楼层是只更新direction,将要出发前才重新找destination并且更新direction。

第三册作业我在公测中被测出了1个bug,在强测中大寄,主要问题是两个bug。

首先,我再pointOnTheWay()方法中遍历电梯的等待队列waitingPassengers时没有加synchronized,这导致了某一线程用iterator遍历waitingPassengers同时其他线程增删waitingPassengers的元素时产生了ConcurrentModification异常。

其次,我为环形电梯定义的pointOnTheWay()方法导致环形电梯出现了左右横跳乘客上不了电梯的情况。考虑以下输入:

[1.0]1-FROM-A-1-TO-A-1
[1.0]2-FROM-B-1-TO-E-1
[1.0]3-FROM-C-1-TO-A-1
[1.0]4-FROM-D-1-TO-B-1
[1.0]5-FROM-E-1-TO-C-1

假设5个乘客同时被分配到了6号电梯,此时6号电梯内部为空,位于A座1楼,方向direction为1(A-B-C-D-E-A循环),destination为C座1楼,正准备出发,则会发生以下事件。

我的解决方案是简单粗暴地改写环形电梯的pointOnTheWay(Point point)方法,只要电梯的路径中有point,则返回true。

第四章 互测方法

我在互测中采用以下办法:

总体上,电梯单元的互测主要依靠随机轰炸。

第五章 总结感悟

总的来说,电梯单元我的收获有:

标签:Java,乘客,等待,队列,电梯,线程,程序设计,多线程,方法
来源: https://www.cnblogs.com/SunnyXia3579/p/16207722.html