本次做业模拟一个多线程实时单部电梯系统,从标准输入中输入请求信息,程序进行接收和处理,模拟电梯运行,将必要的运行信息经过输出接口进行输出。程序员
电梯楼层:1层到15层,共15层;电梯初始位置:1层;输出信息包括两部分,电梯的状态和人的状态算法
电梯的状态,分为两种,OPEN(开门),CLOSE(关门)安全
开门格式:OPEN-楼层(在开门刚开始输出);关门格式:CLOSE-楼层(在关门刚结束输出)数据结构
人的状态,分为两种,IN(进入电梯),OUT(离开电梯);进入电梯格式:IN-id-楼层,这里的id是这我的本身的id;离开电梯格式:OUT-id-楼层多线程
按照请求进入系统的时间顺序,依次按顺序逐个执行运送任务(单人电梯);并发
主线程进行输入的管理,使用ElevatorInput,负责接收请求并存入队列;优化
开一个线程(电梯线程),用于模拟电梯的活动,从队列中取出请求并进行执行,执行完后继续取继续执行;this
构建一个请求队列,用于管理(从输入得到,并能够交给电梯线程)请求,且须要保证队列是线程安全的。spa
从此次做业开始引入了多线程问题,大大提高了做业难度,之前所接触的程序都是单线程的,按照顺序执行,而如今是多个线程同时并发,对于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();
系统评测提供输入格式的处理,咱们只需根据输入构造接收容器便可,也就是一个请求队列,数据类型是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 }
本次做业比较简单,在逻辑上几乎是没有问题的,也不须要优化,因此基本是逻辑清楚就一遍就过。互测debug的时候也没有找到bug。只要保证电梯运行合法便可,在该停的楼层停,该进人的时候进人,该关门的时候关门。须要注意的是,第一个请求输入起始楼层不必定是1层,因此须要先运行至1层再开门进人,如果1层则直接开门;还有不能同时开关门屡次,也不能往返运行同一个请求。
代码和类都比较少,说明第一次做业真的很顺畅,心情很好。
第二次附加了捎带的条件,考虑能够多人同时乘坐的电梯调度,同时限制了运行时间,必须使用多线程并发模式。
1.主请求选择规则:
若是电梯中没有乘客,将请求队列中到达时间最先的请求做为主请求
若是电梯中有乘客,将其中到达时间最先的乘客请求做为主请求
2.被捎带请求选择规则:
电梯的主请求存在,即主请求到该请求进入电梯时还没有完成
该请求到达请求队列的时间小于等于电梯到达该请求出发楼层关门的截止时间
电梯的运行方向和该请求的目标方向一致。即电梯主请求的目标楼层和被捎带请求的目标楼层,二者在当前楼层的同一侧。
3.注意:
电梯不会连续开关门。即开门关门一次以后,若是请求队列中还有请求,不能当即再执行开关门操做,会先执行请求。
在保证输入条件与第一次做业相同(即不断地进行读取不一样时间和相同时间的输入请求),因为电梯运行时间有限制,须要考虑捎带状况,以减小运行时间(便可以多人乘坐的状况下,将顺路捎带)。
这会带来一些复杂度的考虑,也会带来请求队列和调度的问题,同时也会有数据结构上处理的麻烦,也会有线程安全问题。
Main线程保持不变,而后输入线程和电梯线程并发运行,注意考虑线程安全问题,以及线程之间的关系,输入线程是将请勿放入队列,而电梯线程是从请求队列得到请求。
输入线程和第一次没有多少改变,我在输入结束后增长一个标记,该标记是电梯中止ElevatorStop类的对象.
eleInput.close();
stop.runStop(); //stop flag
调度器在做业中起到管理请求队列,与输入线程和电梯线程之间进行数据交互,输入线程输入的请求在调度器的队列中保存,电梯线程又从中取出执行。
调度器自己不是线程,但被输入和电梯两个线程并发使用这个对象。
我此次做业用了一个输入保存请求队列和一个电梯里运行的请求队列。
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 }
电梯线程在第一次基础上增长捎带的功能,同时从调度器中得到请求,若是有知足捎带的请求,继续添加到运行队列中,一并处理。
捎带是本次做业的难点,较为复杂的算法,Bug也是这个地方出现的,代码也是这里开始增加的。
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
我在这里优化为靠近当前运行方向楼层的最早出电梯,这是较好的算法。
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让我没有经过,痛哭啊!!!本次做业的调度算法增长了难度,须要考虑时间优化,因此逻辑上、优化上出现了不少问题。
捎带状况和优化如上图,有一点就是在输入线程时,第一次输入可能有多个输入同时间输入,就必须在第一次同时被执行,这一点难题折磨了我许久,直到最后的截止时间。首先保证线程安全,而后因为我是读一个请求就唤醒电梯线程,此时尚未第二个请求读入,但有可能它们是同一时间但分步读入的请求,电梯就会判断不到有知足捎带的请求,所以默认不捎带了。增长了负楼层,且没有零层,这和实际生活相符,不过这一点只是增长了条件判断,不会特别难。须要注意一下方向和运行停靠问题便可。
本次代码仍是比较长的,增长了调度器类,电梯线程中止类,实质上的多线程就这样开始了。心情瞬间凉了,难受。逻辑性更强了,还增长了多种状况,须要各类方法来调度。
类中的方法变多的缘由是数据结构的处理,还有调度器与两个线程之间的数据交互,还有捎带状况调度问题。
本次做业,须要完成的任务为多部多线程智能(SS)调度电梯的模拟,在第二次做业基础上增长至3部电梯同时运行,且每部电梯的工做状态、工做条件和工做需求都不相同。
电梯可运行楼层:-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名
任何电梯均可以在任意合法的楼层ARRIVE,可是只能在本身所被容许停靠的楼层进行停靠;
电梯任什么时候候,内部的人数都必须小于等于轿厢容量限制。也就是说,即使在OPEN、CLOSE中间,也不容许出现超过容量限制的中间状况;注意请求拆分后的执行顺序,不能跨越也不能超前执行。
本次做业中,乘客须要在合适的楼层中途更换电梯,因此须要将请求按照必定的规则进行拆分处理,分给其他电梯一块儿工做。不过若是这样作的话,必需要考虑多部电梯前后顺序的控制。
构建一个调度器(本次的调度器能够和队列是一体的)
用于管理请求
和电梯进行交互并指派任务给电梯,并可能须要处理请求的前后顺序依赖关系
且须要保证调度器是线程安全的
构建三个电梯线程,每一个电梯的行为功能是同样的,只是三个不一样对象,各自属性成员有相应的区别:运行时间,载荷量。
Main线程依旧保持不变,增长了三个电梯线程。
第二次做业的调度器已经再也不知足全部的需求了,但不是没有用,依然保留原有的调度器,用来管理请求队列,同时我增长了一个请求拆分的类,用来对请求进行判断拆分,还增长了数据结构,即对应电梯各自的运行队列。
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
经过对请求的起始楼层和到达楼层进行判断,看是否可以直接用1个电梯执行,或者更优的电梯运行,同时要考虑到电梯容量,若是A电梯容量满了,它知足B电梯,那么能够交给B电梯,这是优化思想。也能够继续等待A电梯执行完成,再继续执行,毕竟A电梯是最快的电梯。分割厚须要保证请求的执行顺序,若分割为A电梯-3->1,C电梯1->3层,则必须等A电梯将请求执行完成,才能让C电梯执行。
此次做业我没有所有完成,因此确定还存在许多问题,没有进入互测阶段。以前遇到的状况就是多个电梯线程没法停下来,这个问题困扰了我半天的时间,最后请求同窗才解决的,这些问题以前没有遇到过,博客里也没有详细的说明,课上更没有提到,因此踩到这个坑真的没办法。同时wait,和notifyall的使用须要注意,我一直觉得这个是唤醒当前调用它的对象以外的线程,但不是这样,这个是针对当前锁住的对象,只要访问该对象的数据,就属于等待或者被唤醒,这是默认状况。咱们在使用的过程当中能够在前面用对象调用这两个方法,就能够指定电梯线程的唤醒了:eleA.notify().
其余bug就是逻辑问题,此次增长了请求拆分,这个状况比较复杂,判断条件多,因此是bug增多的地方。
代码长度在这个单元里增长了许多,一个是队列数据结构的操做,好在数据结构操做能够复用代码,且处理简单。可是在多线程处理的代码上会比较纠结,以及队请求的分割操做是最麻烦,且逻辑最容易出错的地方。调度问题使得代码行数大大增长。
复杂度就不是一张图能说明的了,而是三张图,可见,这道题的处理是真的复杂,在代码构造的途中不断进行封装,类也比较多,方法也是不少。