本单元做业,由简到难地实现了三版目的选层电梯。本单元主要学习java多线程的编程方法。java
难度逐渐加难,对线程安全性的要求也愈来愈高,下面分别分析这三次做业的设计,性能与线程安全处理。算法
第一次做业要求实现一部多线程的先来先服务电梯,目的是学习多线程编程语法。此次做业难度相对简单,我采用了生产消费模型,请求队列采用阻塞队列实现,避免了本身实现锁,提高了线程安全性。编程
RequestReceiver类是生产者,负责像请求队列中添加请求,PersonRequestBox类将jar包中的请求类包装起来,能够添加结束域实现线程的终止。 Elevator类是消费者,从请求队列中取出请求并执行。因为本次做业实现的算法是先来先服务,即FIFO,能够考虑用队列的数据结构,加之线程安全性的考虑,我采用了BlockingQueue来实现,不须要本身实现请求队列的阻塞了,因为请求人数的不肯定性,我采用了LinkedBlockingQueue来实现。具体的类间关系图以下。安全
<img src="https://img2018.cnblogs.com/blog/1389272/201904/1389272-20190421123608785-1622202358.png" width = "400" div align=center /> <img src="https://img2018.cnblogs.com/blog/1389272/201904/1389272-20190421134358669-215072662.png" width = "200" div align=center />数据结构
线程的结束方法,生产者线程在接收到文件结束符以后构造一个结束域置位的PersonRequestBox放入请求队列,时候生产者线程可结束。当消费者接收到这个特殊的请求时表示以后没有请求了,消费者线程结束。多线程
第二次做业要求实现一部多线程的可捎带电梯,因为测评的缘由,我彻底按照指导书写了一个ALS算法。常见的调度算法有Scan算法,Look算法等,我以为吧其实ALS算法虽然在强测中性能分最低,可是这个算法的实现难度其实比较高,可以更好地练习到面向对象的多线程编程。函数
本次做业基本沿用上次做业的生产消费模型,生产者为 RequestReceiver,消费者为Elevator。此次的请求队列不能再是一个队列了,由于要捎带,不能用仅支持FIFO的容器,因此我采用了Vector类做为请求队列,经过手动加锁的方式实现线程安全。具体的类间关系图以下。性能
<img src="https://img2018.cnblogs.com/blog/1389272/201904/1389272-20190421122910229-632605023.png" width = "400" div align=center />学习
<img src="https://img2018.cnblogs.com/blog/1389272/201904/1389272-20190421123228575-172259041.png" width = "200" div align=center />测试
本次新加入调度器类Controller,这个类来控制请求队列。因此整个类都须要考虑线程安全问题。因为调度器只有一个,我采用单例模式使用Controller,并将Controller中因此操做Vector的方法synchronize住。这样便实现了线程安全。调度器并不是线程,生产类和电梯类调用controller单例对象的方法来添加和取出请求。
第三次做业要实现多部多限制多线程可捎带电梯,此次做业我改进了ALS算法,采用当前孤立最优算法选择电梯运行方向,从而选择主请求,代码结构复用第二次做业。
因为三部电梯可停靠楼层不一样,致使每一个电梯可处理不一样请求,且某些请求须要两部电梯合做换乘才能知足,由此我根据不一样电梯可否知足请求,构造了八个请求队列,分别为A、B、C、AB、AC、BC、ABC、都不能运。以A电梯为例,A的队列其实是A、AB、AC、ABC,这样就能够转换为第二次做业的一个队列了。
对于须要换乘的请求,采用固定的方式将请求拆分,前一部电梯会将请求运到他能运到的最远处,再让后一部电梯去接应。这要求请求是链式可扩展的,我将PersonRequestBox类中增添了nextRequest域,达到了链式存储的效果。
线程安全问题的解决和上次基本相同,将Controller类设置为单例模式,而后将其中操控请求队列的方法synchrnize住。下面是具体的类间关系图。
<img src="https://img2018.cnblogs.com/blog/1389272/201904/1389272-20190421123957910-1217313846.png" width = "600" div align=center />
<img src="https://img2018.cnblogs.com/blog/1389272/201904/1389272-20190421123828557-603072523.png" width = "250" div align=center />
我在第三次做业中对ALS算法进行了优化,达到了不错的性能效果。解决的问题有如下两点
对于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)$和$min(n_2t, n_1f)$中较小者即$min(M_{dn}, N_{dn})$
最后将$n$指令中要向上走的人运到目的地,最远要走到的楼层为$max(n_1t)$记为$N_{up}$
因此电梯走的路径为$O\rightarrow M_{up}\rightarrow min(M_{dn}, N_{dn})\rightarrow 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\rightarrow N_{dn}\rightarrow max(M_{up}, N_{up})\rightarrow M_{dn}$
选取
经过计算以上两种状况电梯须要走的楼层总数,选择总数小的方向,并将该方向上将遇到的第一条指令做为主请求。
当一个中转请求到来时,必须前后依靠两个电梯的配合才能完成。这是一个同步问题,不能再前一部电梯没放人的时候就让后一部电梯接走人,也就是说必须当前一部电梯到达中转楼层后才能将后半条请求发给后一部电梯。这就产生了一个可优化的点,若是前一部电梯到达中转楼层后,后一部电梯才开始跑向中转楼层,有时将浪费大量的时间。因此能够充分利用前一部电梯运乘客的时间让后一部电梯先到达中转楼层。我称之为中转配合法。
在Controller类内设置三个中转楼层域,当来中转请求后,解析中转楼层,存入对应的中转楼层域。当某电梯闲下来要wait时,先读一下中转楼层域,若是不为空,则能够"闲逛”(hang out)到中转楼层,在hang out过程当中若是来了刚需请求,直接去执行刚需请求。不然就在中转楼层wait,达到了中转配合的目的。
关于多线程的测试,我采用了手动边缘测试和自动压力测试两种方式。
自动测试经过脚本生成数据,并检查输出是否正确。在此吐槽一下,互测基本上是面向测评机的,由于多线程的程序,互测怎么看八份代码,oo真练写脚本:)
多线程电梯是生产消费模型的经典实例,让请求做为生产者,电梯做为消费者,调度器维护请求队列。调度器要线程安全,保证请求队列知足伯恩斯坦条件。
生产消费模型的核心是托盘类,我用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函数在第二次的基础上还要考虑电梯中的人数限制。
本次做业中的调度器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课的意义何在呢?