BUAA-OO-第二单元作业-电梯调度
第一次单电梯单人
题目要求
本次作业模拟一个多线程实时单部电梯系统,从标准输入中输入请求信息,程序进行接收和处理,模拟电梯运行,将必要的运行信息通过输出接口进行输出。
设定
电梯楼层:1层到15层,共15层;电梯初始位置:1层;输出信息包括两部分,电梯的状态和人的状态
电梯的状态,分为两种,OPEN(开门),CLOSE(关门)
开门格式:OPEN-楼层(在开门刚开始输出);关门格式:CLOSE-楼层(在关门刚结束输出)
人的状态,分为两种,IN(进入电梯),OUT(离开电梯);进入电梯格式:IN-id-楼层,这里的id是这个人自己的id;离开电梯格式:OUT-id-楼层
解题思路
按照请求进入系统的时间顺序,依次按顺序逐个执行运送任务(单人电梯);
主线程进行输入的管理,使用ElevatorInput,负责接收请求并存入队列;
开一个线程(电梯线程),用于模拟电梯的活动,从队列中取出请求并进行执行,执行完后继续取继续执行;
构建一个请求队列,用于管理(从输入获得,并可以交给电梯线程)请求,且需要保证队列是线程安全的。
-
多线程问题
从这次作业开始引入了多线程问题,大大提升了作业难度,以前所接触的程序都是单线程的,按照顺序执行,而现在是多个线程同时并发,对于JAVA代码在JVM虚拟机下如何运行的程序员不能看到,只能通过代码和输出结果进行分析,这对于debug来说是一件很头疼的事。
多线程与进程并发是同一个意思,也就是多个线程互相争抢CPU资源,同时会共享部分数据,因此需要考虑同步与互斥问题。
其实我们之前写的程序中Main方法就是一个调度整个代码文件的线程,整个工程运行时就是从Main方法开始进行单线程运行。这次作业的Main线程主要负责创建输入对象和电梯对象,进行交互,然后输入类、电梯类处理自己的事情,降低代码耦合度。
下面用Thread线程类创建线程并调用线程运行。
1 Thread input = new Thread(new Input()); 2 Thread elevator = new Thread((new Elevator())); 3 input.start(); 4 elevator.start();
-
输入线程(本次作业该线程我没有继承Thread类,也没有实现调度器)
系统评测提供输入格式的处理,我们只需根据输入构造接收容器即可,也就是一个请求队列,数据类型是PersonRequest,从而获取request的属性和数据。
由于考虑输入数据的时间和数量是无法确定的,所以我们把线程进入死循环,当人工运行输入手动结束或者评测机加结束符时为结束条件,退出循环,关闭输入,停止线程运行。
Queue<PersonRequest> requestQ = new LinkedList<PersonRequest>();
-
电梯线程
首先,电梯需要构造自己的属性和方法,格式上与request保持一致。然后由于电梯需要与其他线程并发,所以电梯继承Thread类。
第一次电梯运行处理比较简单,逐个获取请求队列中的请求,然后逐个执行即可,在运行时间上不用考虑优化。当请求队列为空且输入停止时停止线程,程序结束。
电梯这个类中不仅需要获得请求,还要将请求进行“运送”,也就是执行请求(电梯本质的功能):运行至请求输入楼层开门,电梯进人,关门运行,到达开门,再关门……
1 private int personId; 2 private int fromFloor; 3 private int toFloor; 4 private int nowFloor; 5 6 private int getFloor; 7 8 private static final int optime = 200; 9 private static final int cltime = 200; 10 private static final int runtime = 400; 11 12 public RunElevator(int personId, int fromFloor, 13 int toFloor, int nowFloor) { 14 this.personId = personId; 15 this.fromFloor = fromFloor; 16 this.toFloor = toFloor; 17 this.nowFloor = nowFloor; 18 }
2.遇到bug
本次作业比较简单,在逻辑上几乎是没有问题的,也不需要优化,所以基本是逻辑清楚就一遍就过。互测debug的时候也没有找到bug。只要保证电梯运行合法即可,在该停的楼层停,该进人的时候进人,该关门的时候关门。需要注意的是,第一个请求输入起始楼层不一定是1层,所以需要先运行至1层再开门进人,若是1层则直接开门;还有不能同时开关门多次,也不能往返运行同一个请求。
类图分析
代码和类都比较少,说明第一次作业真的很顺畅,心情很好。
代码行数
代码复杂度分析
第二次单电梯捎带多调度
题目要求
第二次附加了捎带的条件,考虑可以多人同时乘坐的电梯调度,同时限制了运行时间,必须使用多线程并发模式。
1.主请求选择规则:
如果电梯中没有乘客,将请求队列中到达时间最早的请求作为主请求
如果电梯中有乘客,将其中到达时间最早的乘客请求作为主请求
2.被捎带请求选择规则:
电梯的主请求存在,即主请求到该请求进入电梯时尚未完成
该请求到达请求队列的时间小于等于电梯到达该请求出发楼层关门的截止时间
电梯的运行方向和该请求的目标方向一致。即电梯主请求的目标楼层和被捎带请求的目标楼层,两者在当前楼层的同一侧。
3.注意:
电梯不会连续开关门。即开门关门一次之后,如果请求队列中还有请求,不能立即再执行开关门操作,会先执行请求。
解题思路
在保证输入条件与第一次作业相同(即不断地进行读取不同时间和相同时间的输入请求),由于电梯运行时间有限制,需要考虑捎带情况,以减少运行时间(即可以多人乘坐的情况下,将顺路捎带)。
这会带来一些复杂度的考虑,也会带来请求队列和调度的问题,同时也会有数据结构上处理的麻烦,也会有线程安全问题。
1.多线程问题
Main线程保持不变,然后输入线程和电梯线程并发运行,注意考虑线程安全问题,以及线程之间的关系,输入线程是将请勿放入队列,而电梯线程是从请求队列获得请求。
2.输入线程(本次作业必须使用该线程并发,我用了Runnable接口)
输入线程和第一次没有多少改变,我在输入结束后增加一个标记,该标记是电梯停止ElevatorStop类的对象.
eleInput.close();
stop.runStop(); //stop flag
3.调度器与请求队列
调度器在作业中起到管理请求队列,与输入线程和电梯线程之间进行数据交互,输入线程输入的请求在调度器的队列中保存,电梯线程又从中取出执行。
调度器本身不是线程,但被输入和电梯两个线程并发使用这个对象。
我这次作业用了一个输入保存请求队列和一个电梯里运行的请求队列。
private ArrayList<PersonRequest> requestV = new ArrayList<>(); // input
private Vector<PersonRequest> runRequest = new Vector<>(); // run
这里是线程考虑加锁,保住安全性。
电梯获取请求的方法getRequest,当队列为空时,如果输入结束直接返回空标志线程结束,输入未结束则电梯线程等待。
1 public synchronized void add(PersonRequest request) { 2 requestV.add(request); 3 notifyAll(); 4 } 5 public synchronized boolean isEmpty() { 6 return requestV.isEmpty(); 7 } 8 public synchronized PersonRequest getRequest() { 9 try { 10 while (isEmpty()) { 11 if (stop.getStop() && isEmpty()) { 12 return null; 13 } else { 14 wait(); 15 } 16 } 17 return requestV.get(0); 18 } catch (Exception e) { 19 e.printStackTrace(); 20 return null; 21 } 22 }
4.电梯线程
电梯线程在第一次基础上增加捎带的功能,同时从调度器中获得请求,如果有满足捎带的请求,继续添加到运行队列中,一并处理。
5.捎带调度
捎带是本次作业的难点,较为复杂的算法,Bug也是这个地方出现的,代码也是这里开始增长的。
(1)第一种情况就是考虑当前是否有多个输入满足同一楼层出发,那么就是先取出一个请求,然后判断当前的输入队列是否满足条件。
1 public synchronized int proRequest(int nowfloor) { //return equal nowfloor 2 int i; 3 for (i = 0; i < requestV.size(); i++) { 4 if (requestV.get(i).getFromFloor() == nowfloor) { 5 return i; 6 } 7 } 8 return -1; 9 } 10 11
(2)当电梯捎带之后,我们就需要考虑请求的先后顺序了,到底谁先下电梯呢?
我在这里优化为靠近当前运行方向楼层的最先出电梯,这是较好的算法。
1 public int getDown() { // return highest floor of down elevator 2 int i; 3 int k = -3; 4 int index = -1; 5 for (i = 0; i < runRequest.size(); i++) { 6 if (runRequest.get(i).getToFloor() >= k) { 7 k = runRequest.get(i).getToFloor(); 8 index = i; 9 } 10 } 11 return index; 12 } 13 14 public int getPro() { // return lowest floor of up elevator 15 int i; 16 int k = 16; 17 int index = -1; 18 for (i = 0; i < runRequest.size(); i++) { 19 if (runRequest.get(i).getToFloor() <= k) { 20 k = runRequest.get(i).getToFloor(); 21 index = i; 22 } 23 } 24 return index; 25 }
遇到bug
这次不是特别开心,因为一个bug让我没有通过,痛哭啊!!!本次作业的调度算法增加了难度,需要考虑时间优化,所以逻辑上、优化上出现了很多问题。
捎带情况和优化如上图,有一点就是在输入线程时,第一次输入可能有多个输入同时间输入,就必须在第一次同时被执行,这一点难题折磨了我许久,直到最后的截止时间。首先保证线程安全,然后由于我是读一个请求就唤醒电梯线程,此时还没有第二个请求读入,但有可能它们是同一时间但分步读入的请求,电梯就会判断不到有满足捎带的请求,因此默认不捎带了。增加了负楼层,且没有零层,这和实际生活相符,不过这一点只是增加了条件判断,不会特别难。需要注意一下方向和运行停靠问题即可。
类图分析
本次代码还是比较长的,增加了调度器类,电梯线程停止类,实质上的多线程就这样开始了。心情瞬间凉了,难受。逻辑性更强了,还增加了多种情况,需要各种方法来调度。
代码行数
代码复杂度分析
类中的方法变多的原因是数据结构的处理,还有调度器与两个线程之间的数据交互,还有捎带情况调度问题。
第三次多电梯多调度
题目要求
本次作业,需要完成的任务为多部多线程智能(SS)调度电梯的模拟,在第二次作业基础上增加至3部电梯同时运行,且每部电梯的工作状态、工作条件和工作需求都不相同。
基本设定
1.电梯数量:3部,分别编号为A,B,C
电梯可运行楼层:-3-1,1-20具有负层,且每个电梯运行停靠楼层不同:
A: -3, -2, -1, 1, 15-20 B: -2, -1, 1, 2, 4-15 C: 1, 3, 5, 7, 9, 11, 13, 15
电梯上升或下降一层的时间也不同:
A: 0.4s B: 0.5s C: 0.6s
电梯最大载客量(轿厢容量)还是不同:
A:6名 B:8名 C:7名
2.输出格式增加了运行电梯的编号,即该请求被哪个电梯所运行。
3.注意
任何电梯都可以在任意合法的楼层ARRIVE,但是只能在自己所被允许停靠的楼层进行停靠;
电梯任何时候,内部的人数都必须小于等于轿厢容量限制。也就是说,即便在OPEN、CLOSE中间,也不允许出现超过容量限制的中间情况;注意请求拆分后的执行顺序,不能跨越也不能超前执行。
解题思路
本次作业中,乘客需要在合适的楼层中途更换电梯,所以需要将请求按照一定的规则进行拆分处理,分给其余电梯一起工作。不过如果这样做的话,必须要考虑多部电梯先后顺序的控制。
构建一个调度器(本次的调度器可以和队列是一体的)
用于管理请求
和电梯进行交互并指派任务给电梯,并可能需要处理请求的先后顺序依赖关系
且需要保证调度器是线程安全的
构建三个电梯线程,每个电梯的行为功能是一样的,只是三个不同对象,各自属性成员有相应的区别:运行时间,载荷量。
1.多线程问题
Main线程依旧保持不变,增加了三个电梯线程。
2.输入线程(和第二次一样)
3.调度器与请求队列
第二次作业的调度器已经不再满足所有的需求了,但不是没有用,依然保留原有的调度器,用来管理请求队列,同时我增加了一个请求拆分的类,用来对请求进行判断拆分,还增加了数据结构,即对应电梯各自的运行队列。
4.多部电梯线程
1 Thread eleA = new Thread(elevatorA); 2 Thread eleB = new Thread(elevatorB); 3 Thread eleC = new Thread(elevatorC); 4 eleA.start(); 5 eleB.start(); 6 eleC.start(); 7 8 private long runtime; // 电梯运行时间不同 9 private int volume; // 电梯载客不同 10 private String elename; // 电梯标号 11 private int nowvolume = 0; // 当前载客量 12 13
5.捎带调度保持与第二次不变
6.请求分割问题
通过对请求的起始楼层和到达楼层进行判断,看是否能够直接用1个电梯执行,或者更优的电梯运行,同时要考虑到电梯容量,如果A电梯容量满了,它满足B电梯,那么可以交给B电梯,这是优化思想。也可以继续等待A电梯执行完成,再继续执行,毕竟A电梯是最快的电梯。分割厚需要保证请求的执行顺序,若分割为A电梯-3->1,C电梯1->3层,则必须等A电梯将请求执行完成,才能让C电梯执行。
遇到bug
这次作业我没有全部完成,所以肯定还存在许多问题,没有进入互测阶段。之前遇到的情况就是多个电梯线程无法停下来,这个问题困扰了我半天的时间,最后请求同学才解决的,这些问题之前没有遇到过,博客里也没有详细的说明,课上更没有提到,所以踩到这个坑真的没办法。同时wait,和notifyall的使用需要注意,我一直以为这个是唤醒当前调用它的对象之外的线程,但不是这样,这个是针对当前锁住的对象,只要访问该对象的数据,就属于等待或者被唤醒,这是默认情况。我们在使用的过程中可以在前面用对象调用这两个方法,就可以指定电梯线程的唤醒了:eleA.notify().
其他bug就是逻辑问题,这次增加了请求拆分,这个情况比较复杂,判断条件多,所以是bug增多的地方。
类图分析
一共8个类:
代码行数
代码长度在这个单元里增加了许多,一个是队列数据结构的操作,好在数据结构操作可以复用代码,且处理简单。但是在多线程处理的代码上会比较纠结,以及队请求的分割操作是最麻烦,且逻辑最容易出错的地方。调度问题使得代码行数大大增加。
代码复杂度分析
复杂度就不是一张图能说明的了,而是三张图,可见,这道题的处理是真的复杂,在代码构造的途中不断进行封装,类也比较多,方法也是很多。