面向对象目的选层电梯作业总结
一、综述
本单元作业,由简到难地实现了三版目的选层电梯。本单元主要学习java多线程的编程方法。
- 第一次:多线程的傻瓜式单电梯
- 第二次:多线程可捎带电梯
- 第三次:多线程多部多限制可捎带电梯。
难度逐渐加难,对线程安全性的要求也越来越高,下面分别分析这三次作业的设计,性能与线程安全处理。
二、基于度量来分析自己的程序
第一次作业
第一次作业要求实现一部多线程的先来先服务电梯,目的是学习多线程编程语法。这次作业难度相对简单,我采用了生产消费模型,请求队列采用阻塞队列实现,避免了自己实现锁,提升了线程安全性。
RequestReceiver类是生产者,负责像请求队列中添加请求,PersonRequestBox类将jar包中的请求类包装起来,可以添加结束域实现线程的终止。 Elevator类是消费者,从请求队列中取出请求并执行。由于本次作业实现的算法是先来先服务,即FIFO,可以考虑用队列的数据结构,加之线程安全性的考虑,我采用了BlockingQueue来实现,不需要自己实现请求队列的阻塞了,由于请求人数的不确定性,我采用了LinkedBlockingQueue来实现。具体的类间关系图如下。
线程的结束方法,生产者线程在接收到文件结束符之后构造一个结束域置位的PersonRequestBox放入请求队列,时候生产者线程可结束。当消费者接收到这个特殊的请求时表示之后没有请求了,消费者线程结束。
第二次作业
第二次作业要求实现一部多线程的可捎带电梯,由于测评的原因,我完全按照指导书写了一个ALS算法。常见的调度算法有Scan算法,Look算法等,我觉得吧其实ALS算法虽然在强测中性能分最低,但是这个算法的实现难度其实比较高,能够更好地练习到面向对象的多线程编程。
本次作业基本沿用上次作业的生产消费模型,生产者为 RequestReceiver,消费者为Elevator。这次的请求队列不能再是一个队列了,因为要捎带,不能用仅支持FIFO的容器,所以我采用了Vector类作为请求队列,通过手动加锁的方式实现线程安全。具体的类间关系图如下。
本次新加入调度器类Controller,这个类来控制请求队列。所以整个类都需要考虑线程安全问题。由于调度器只有一个,我采用单例模式使用Controller,并将Controller中所以操作Vector的方法synchronize住。这样便实现了线程安全。调度器并非线程,生产类和电梯类调用controller单例对象的方法来添加和取出请求。
第三次作业
第三次作业要实现多部多限制多线程可捎带电梯,这次作业我改进了ALS算法,采用当前孤立最优算法选择电梯运行方向,从而选择主请求,代码结构复用第二次作业。
由于三部电梯可停靠楼层不同,导致每个电梯可处理不同请求,且某些请求需要两部电梯合作换乘才能满足,由此我根据不同电梯能否满足请求,构造了八个请求队列,分别为A、B、C、AB、AC、BC、ABC、都不能运。以A电梯为例,A的队列实际上是A、AB、AC、ABC,这样就可以转换为第二次作业的一个队列了。
对于需要换乘的请求,采用固定的方式将请求拆分,前一部电梯会将请求运到他能运到的最远处,再让后一部电梯去接应。这要求请求是链式可扩展的,我将PersonRequestBox类中增添了nextRequest域,达到了链式存储的效果。
线程安全问题的解决和上次基本相同,将Controller类设置为单例模式,然后将其中操控请求队列的方法synchrnize住。下面是具体的类间关系图。
三、优化算法
我在第三次作业中对ALS算法进行了优化,达到了不错的性能效果。解决的问题有以下两点
- 当从调度器中获得主请求时是按照先来先服务的原则选的,这在大概率上会导致电梯的总运行时间不一定是最优的。
- 当换乘请求到来时,如果后一部电梯空闲可以事先让其到达中转楼层等待,减少换乘时间。
实现方式
1. 当前孤立最优算法
对于ALS算法来说,电梯会在内部无人的时候从请求队列中选取主请求。由于可捎带,实际上主请求的选择可以等价于选择电梯接下来的运行方向,主请求自然就是该方向上最先遇到的请求了。
那么如何选择方向呢?我采取的算法是当前孤立最优算法,所谓当前最优就是不考虑以后的请求到来情况,孤立就是不考虑与其他电梯抢任务的问题,只考虑这一部电梯干完目前出现的所有请求是先向上走快还是先向下走快。
约定
设当前电梯所在楼层为O,O层上方共有m条请求,其中有(m_1)条要向上走,(m_2)条要向下走,O层下方共有n条请求,其中有(n_1)条要向上走,(n_2)条要向下走。(m/n_{1/2} f/t)表示指令的from楼层或to楼层,例如(m_1f)表示O上方的要向上走的请求的from楼层,其它同理。
- 将(max(m_1t, m_2f))记为(M_{up})
- 将(min(m_2t))记为(M_{dn})
- 将(max(n_1t))记为(N_{up})
- 将(min(n_2t, n_1f))记为(N_{dn})
电梯先向上运行
需向上走到的最远楼层为(max(m_1t, m_2f))即(M_{up})
之后再下到下方最远楼层为(min(m_2t))和(min(n_2t, n_1f))中较小者即(min(M_{dn}, N_{dn}))
最后将(n)指令中要向上走的人运到目的地,最远要走到的楼层为(max(n_1t))记为(N_{up})
所以电梯走的路径为(O ightarrow M_{up} ightarrow min(M_{dn}, N_{dn}) ightarrow N_{up})
电梯先向下运行
需向下走到的最远楼层为(min(n_2t, n_1f))即(N_{dn})
之后再上到方最远楼层为(max(n_1t))和(max(m_1t, m_2f))中较大者即(max(M_{up}, N_{up}))
最后将(m)指令中要向下走的人运到目的地,最远要走到的楼层为(min(m_2t))记为(M_{dn})
所以电梯走的路径为(O ightarrow N_{dn} ightarrow max(M_{up}, N_{up}) ightarrow M_{dn})
选取
通过计算以上两种情况电梯需要走的楼层总数,选择总数小的方向,并将该方向上将遇到的第一条指令作为主请求。
2. 中转配合法
当一个中转请求到来时,必须先后依靠两个电梯的配合才能完成。这是一个同步问题,不能再前一部电梯没放人的时候就让后一部电梯接走人,也就是说必须当前一部电梯到达中转楼层后才能将后半条请求发给后一部电梯。这就产生了一个可优化的点,如果前一部电梯到达中转楼层后,后一部电梯才开始跑向中转楼层,有时将浪费大量的时间。所以可以充分利用前一部电梯运乘客的时间让后一部电梯先到达中转楼层。我称之为中转配合法。
在Controller类内设置三个中转楼层域,当来中转请求后,解析中转楼层,存入对应的中转楼层域。当某电梯闲下来要wait时,先读一下中转楼层域,如果不为空,则可以"闲逛”(hang out)到中转楼层,在hang out过程中如果来了刚需请求,直接去执行刚需请求。否则就在中转楼层wait,达到了中转配合的目的。
四、测试部分
关于多线程的测试,我采用了手动边缘测试和自动压力测试两种方式。
自动测试通过脚本生成数据,并检查输出是否正确。在此吐槽一下,互测基本上是面向测评机的,因为多线程的程序,互测怎么看八份代码,oo真练写脚本:)
五、Applying Creational Pattern
1. 生产消费模型
多线程电梯是生产消费模型的经典实例,让请求作为生产者,电梯作为消费者,调度器维护请求队列。调度器要线程安全,保证请求队列满足伯恩斯坦条件。
生产消费模型的核心是托盘类,我用Controller类来实现,第二次作业中的Controller类采用如下公有方法:
public synchronized void addList(PersonRequestBox pb);
public synchronized PersonRequestBox peekMainRequest();
public synchronized PersonRequestBox getMainRequest();
public synchronized boolean isPickable(int floor, boolean direction);
public synchronized List<PersonRequestBox> getPopPickList(int floor, boolean direction);
其中addList方法用来向队列中添加请求,peekMainRequest和getMainRequest是提供电梯对主请求的操作,一个是查看但不取出主请求,另一个是从队列取出并删除主请求。isPickable和getPopPickList是提供电梯对捎带请求对操作,isPickable通过遍历请求队列,返回是否本层有可捎带的请求,getPopPickList将本层可捎带的请求从队列中删除并返回。
第三次作业我采用了8个请求队列的方式,将第二次作业的Controller类更名为ControlList,负责管理一个请求队列,而Controller类中包含8个ControlList对象,管理8个请求队列。Controller中的公有方法如下:
public synchronized void addList(PersonRequestBox pb);
public synchronized PersonRequestBox getMainRequest(int id, int f);
public synchronized PersonRequestBox takeMainRequestS(int id, int floor, boolean direction);
public synchronized boolean isPickable(int floor, boolean direction, int id, int maxNum);
public synchronized List<PersonRequestBox> getPopPickList(int floor,
boolean direction, int id, int maxNum);
和第二次作业的方法功能基本相同,addList用来添加请求,其要解析请求并将其添加到对应的请求队列中,其中的中转请求要被拆分成链式请求存到前一部电梯的请求队列中。getMainRequest和takeMainRequestS两个方法提供电梯对主请求对操作,前者通过当前孤立最优算法找到并返回主请求,后者是为了解决电梯在得到同层反向主请求时开关两次门的问题而设置的一个原子操作函数,在第二次作业中因为只有一部电梯所以这个功能不需要原子性,本次中多个电梯可能会抢公共队列中的请求,所以必须用一个原子的方法解决这个问题。最后的isPickable和getPopPickList函数在第二次的基础上还要考虑电梯中的人数限制。
2. 单例模式
本次作业中的调度器Controller类只需要且只能有一个实例,所以将其定义为单例模式很满足安全性,因为假如自己或其他人在使用时将电梯类和生产者类采用了不同的调度器对象,将产生错误的结果,所以单例模式的设计是合理的。
public class Controller {
private Vector<PersonRequestBox> requestList;
private static Controller controller = new Controller();
private Controller() {
requestList = new Vector<>();
}
public static synchronized Controller getController() {
return controller;
}
}
因为程序运行一开始就需要加载调度器,所以直接采用饿汉式在一开始就构造调度器对象。调用者通过getController方法获得调度器对象的引用,这样就可以通过该引用使用Controller类的方法了。
六、总结
总体来说,这三次作业中学到了java多线程程序的编写方法,掌握了保护线程安全性应用到的基本方法,包括jdk实现的线程安全类如BlockingQueue和自己用对象锁synchronize来实现自己的线程安全类。但遗憾的是,第二次作业受指导书规则影响过度谨慎,后来规则更改后对我采取的决策完全不利,虽然没有bug,但性能分比较低,反正学到了东西就好吧,也希望oo课能在我们的共同努力下越来越完善。
最后想说一点我对于6系oo课的想法,因为我舍友在软件学院修oo课,他们的课程实验让他们学到了很多oo语法,像内部类,Lock类这种,他们是通过实验报告的方式让大家学习这些知识的,而我们的oo课更注重实现功能,互相测bug,导致我们在写完这些作业后还是不会用甚至不知道有这些语法。课程的目的是学知识,如果我们比他们恶心那么多的oo课还没有他们学的东西多范围广,那我们oo课的意义何在呢?