2019面向对象程序设计——目的选乘电梯之优化篇java
做者:1723 🐺算法
优化前言:安全
通过了第二单元三次电梯的历练,可能不会有特别多的人比我更加深切体会到一个优秀架构的重要性。由于在优化策略上耗费巨大心血的我,虽然强测测试点的确拿了一些100,但却第二次电梯做业却由于架构设计上的不足和犯懒没作更多的自主测试,强测惨痛爆点,直奔C屋,捡了芝麻丢了西瓜。在第二次做业后,我在架构设计上进行了许多思考,而且普遍参考了许多16级和17级高工巨佬们的架构设计。在保证正确性的基础上,优化策略的价值便体现出来,所以,本文也会介绍一些优化策略。架构
一个优秀的架构也许不足以让你强测拿顶级分,若没有优秀的架构,对于数据较少的强测,也许能拿顶级分,但对于大批量数据测试,性能和安全性的问题就会暴露出来。对多电梯来讲,架构设计尤为重要。机器学习
一.架构设计要点:主线程退出问题性能
这个问题和输入线程、调度器、电梯的死亡问题不是同一个问题。学习
在第一次做业中,指导书中在“输入接口.md”中明确指出:建议单独开一个线程处理输入。而在以后的第三次指导书中,则改成了:建议主线程处理输入。我猜想,这个修改多是因为大多数同窗在拥有了输入线程以后,却忽视了主线程的做用。测试
对于单开输入线程的同窗,这个设计要点,你可能会感到诧异:个人主线程负责创造其余线程,创造完以后,它就死了,不会形成其余影响,也不会引发正确性的问题啊?优化
没错,确实可能在此次做业中不会引发正确性上的问题,甚至那些难以复现的bug也不是所以而产生,但这不符合工程规范,架构的逻辑性也存在问题。this
下面这句话,是一个优秀架构所须要的重要设计规范之一:(注:这句话不是我说的,源于一位大佬助教的分享)
保证全部非主线程的生命周期都直接或间接被主线程严密控制,即主线程第一个出生,最后一个死亡。
如何能保证主线程最后一个死亡?方法之一就是线程的联合:即join()方法。
在此仅简介,具体请本身深刻研究。
一个线程A在占有CPU资源期间,可让其余线程调用join()方法和本线程联合,例如B.join();称A在运行期间联合了B。若是线程A在占有CPU资源期间联合了B线程,那么A线程将马上中断执行,一直等到它联合的线程B执行完毕,A线程再从新排队等待CPU资源,以便恢复执行。
以第二次做业为例,除了主线程以外,因为电梯线程是最后死亡的,因此主线程在执行最后一条语句以前联合电梯线程,在电梯线程死亡后再死亡。
//建立调度器线程 Dispatch dispatch = new Dispatch(eleNum, inputDeadFlag, dispatchDeadFlag, upReqList, downReqList); //建立电梯线程 Elevator elevator = new Elevator(1, dispatchDeadFlag); //建立输入线程 InputThread inputThread = new InputThread( upReqList, downReqList, inputDeadFlag); dispatch.addElevator(elevator, 0); //start电梯线程 elevator.start(); //start调度器线程 dispatch.start(); //start输入线程 inputThread.start(); try { //主线程联合电梯线程 elevator.join(); } catch (InterruptedException e) { e.printStackTrace(); }
二.架构设计要点:主线程、调度器线程、电梯线程的安全死亡问题
这个问题谈不上架构优化,是全部同窗都必须解决好的一个问题,可是你们的解决方法各不相同。有些原始方法在第2、三次做业中可能会遇到线程安全问题,好比在第三次做业中,直接将 null put到请求队列中,电梯读到null自动终止的方法有多是不安全的(只是对于部分设计来讲不安全)。
如下是一个利用同步锁的我的推荐(并不必定真的好)的方法:
执行逻辑:
输入线程在没有input时终止(即读到null终止);
调度器线程将在输入线程终止且一级请求队列(调度器请求队列)没有待分配的请求时终止;
电梯线程将在调度器终止且二级请求队列(电梯内部请求队列)没有待执行请求且电梯内没人时终止。
线程控制:
托盘对象类:
public class DeadFlag { private boolean deadFlag; DeadFlag() { this.deadFlag = false; } boolean getDeadFlag() { synchronized (this) { return this.deadFlag; } } void setDeadFlag() { synchronized (this) { this.deadFlag = true; } } }
输入线程和调度器共享一个托盘对象;调度器和电梯共享一个托盘对象
//输入线程死亡 try { synchronized (inputDeadFlag) { inputDeadFlag.setDeadFlag(); inputDeadFlag.notifyAll(); } } catch (Exception e) { e.printStackTrace(); }
//调度器线程死亡 if (inputDeadFlag.getDeadFlag() && upReqList.isEmpty() && downReqList.isEmpty()) { break; } //在break以后 synchronized (dispatchDeadFlag) { dispatchDeadFlag.setDeadFlag(); dispatchDeadFlag.notifyAll(); }
//电梯线程死亡 if (dispatchDeadFlag.getDeadFlag() && upReqList.isEmpty() && downReqList.isEmpty() && personIn.isEmpty()) { break alive; }
其实调度器没有必要单开线程,能够做为附属组合模块,这样也会大大下降了线程安全控制的难度,以上是我第二次做业写的电梯之一,调度器线程是为了第三次做业的复用作准备(其实可彻底能够不用)。
三.架构设计要点:CPU时间
关于评测机的CPU超时检测是一大谜题,但一部分仅有wait()¬ifyAll或await()&signalAll()的架构在这强测中出现了CPU连环爆点的状况。(我在本地作过CPU测试,然而仍是被hack了CPU超时.....)
如下是三种下降CPU方法,推荐使用①+③组合或方法②
方法①:wait()+notifyAll()或利用ReentrantLock&Condition类的await()+signalAll()
ReentrantLock&Condition类的await()+signalAll()比wait()+notifyAll()更加灵活精确,推荐尝试。同步方法与加锁在此再也不赘述,详情请见老师上课的课件或上网自学。
方法②(推荐):电梯自主自杀与调度器激活法
这一方法虽然暴力,而且没太用到互斥控制,但通过测试,这种方法下降CPU时间上有奇效,推荐使用(要求使用Runnable接口)。
何谓电梯自主自杀?
答:使用Runnable接口建立的电梯线程在执行完全部请求后自动死亡。
何谓调度器激活?
答:调度器每分发一个请求给电梯后,会调用isAlive()方法检查电梯线程是否alive,若是电梯线程已经死亡,则从新电梯建立一个线程并激活。
具体实现
public class Dispatcher { //初始化,每个elevator对应一个thread,起初都是null。 LinkedList<Elevator> elevators; LinkedList<Thread> threads = new LinkedList<>(); public Dispatcher(LinkedList<Elevator> elevators) { this.elevators = elevators; for (int i = 0; i < elevators.size(); i++) { threads.add(null); } } //剩余部分省略 }
//调度器先把请求加进i号电梯。 getElevators().get(i).addRequest(request); //检查i号电梯对应的线程状态,如果死的线程则为电梯从新建立线程并激活。 if (getThreads().get(i) == null || !getThreads().get(i).isAlive()) { getThreads().set(i,new Thread(getElevators().get(i))); getThreads().get(i).start(); }
方法③:电梯线程sleep(1)
强烈建议想使用方法①,而且把请求分给全部电梯让电梯自由抢的同窗,再使用方法③做为辅助方案。
为何多电梯使用wait()与notifyAll()仍会炸点?我认为缘由有二:
其一:电梯wait()的机会少。
其二:电梯刚wait()就被唤醒。这样电梯会很生气
若是是电梯抢请求,请求分给多个电梯,甚至同一请求按照不一样拆分方式分给电梯,电梯请求队列的请求数目较多,基本不会休息,这样一来,大量线程争夺CPU资源,爆点也在乎料之中。
当咱们设置sleep时,等于告诉CPU,当前的线程再也不运行,持有当前对象的锁。那么这个时候CPU就会切换到另外的线程了,所以让电梯线程sleep(1)也是能缓解CPU时间的一个辅助方法。
四.顶级架构模式:调度策略与电梯运行的彻底抽象剥离
调度策略与电梯运行相剥离的抽象剥离的架构,是顶级设计架构之一,但我在作做业时死🐟安乐并无去尝试,只能过后诸葛,我系计算机专业的wsz巨佬基本实现了这一架构,如下设计参考wsz同窗的思路。
何谓调度策略与电梯运行的彻底抽象剥离?
答:电梯只负责上下楼,调度策略彻底由外部决定
只有不到10行的电梯类!基类Base也只有100行不到,只负责上下楼和安置Strategy。
public class Elevator extends Base { public Elevator() { super(); setStrategy(new Strategy()); } }
Strategy是电梯的调度策略,是外部类,可直接安置给电梯。
为何被普遍承认为顶级架构?
答:你能够把各类sao操做,奇技淫巧,优化策略分别写成一个单独的Strategy类,电梯想换策略直接换一个Strategy类就能够,极大地与Solid原则吻合。
对于不一样的Strategy,其基础功能也能够定义接口
public interface Strategy { LinkedList<Job> getFinishJobs(int curFloor); void addJob(PersonRequest request); String getDirection(int curFloor); boolean isEmpty(); }
若想多策略并行择优选择,这一架构很是合适
五.单电梯优化策略:LOOK算法+条件折返
只谈性能,用户体验为0
LOOK算法的平均运行时间会明显优于纯贪心算法以及先来先服务的ALS算法。使用LOOK算法,强测基本以及能够拿到90的成绩,但LOOK算法自己有性能上的弊端,即不管人怎么怼门,它也不折返。处理好什么时候该折返、什么时候不折返问题,强测数据点甚至能够拿一半以上的满分。
好比:
233-FROM-1-TO-4 2333-FROM-3-TO-4 23333-FROM-8-TO-13
这样的测试点,ALS电梯会明显优于LOOK电梯,若是不加处理,LOOK会优化分爆零。
若是你的电梯沿运行方向已经走过了某个楼层,却有生气的乘客疯狂怼门,你开不开门呢?如下提出两种优化观点,一为模拟,二为预测。
模拟:
看到有dalao已经实现了多策略电梯,即每一个请求给多个电梯都跑一遍,择优输出,这样的电梯性能想必是极强无比的。在这里,我介绍另一种优化观点:模拟。
对于个人LOOK电梯,每作一个方向上的任务时,有三个属性:
private EleStatus status; private EleStatus dirc; //注:初始折返次数:0,最大折返次数:1 private int backtrack;
status表示的是电梯想处理哪一个方向的一趟请求,dirc是电梯当前运行方向,backtrack记录折返次数
电梯每到一层楼时,若是status与dirc是同向的(dirc与status不一样向时意味着电梯正在反向去接最上或最下楼层的请求,接完后开始作任务),且有须要折返的请求时,若这趟任务的折返次数为0,则进行模拟折返和模拟不折返,提早暴力算出完成当前所有请求,折返与不折返的时间开销,不容许一趟请求屡次折返。如何计算并不困难,可直接暴力拿电梯开关门时间,电梯运行一层时间莽算便可。
//用于模拟折返,返回折返稍人的完成所有请求的总运行时间 private int simulateBack(int curFloor);
//用于模拟不折返,返回不折返的完成所有请求的总运行时间 private int simulateNotBack(int curFloor);
以上行举个例子
//条件1 if (dirc.equals(EleStatus.up) && status.equals(EleStatus.up))
//条件2 遍历上行请求队列发现存在request知足request.getFromFloor()<curFloor(当前楼层)
//条件3 this.backtrack = 0;
知足以上请求则进行模拟
显然电梯折返的运行状况是:
status:up->down dirc:down->up->down
不折返的运行状况是:
status:up->down->up dirc:up->down->up
统计代表:电梯折返占优与不折返的占优数据测试为三七开,要想拿那3成的优化分,模拟是很重要的。
预测:
我没这么干,也没能力让电梯机器学习
一个会机器学习的电梯于本次做业是无用的,由于强测数据点很随机。
对于大规模有必定规律性数据的测试,这也与平时生活相似。电梯能够根据实时统计结果或累计统计结果动态调整策略,达到在空乘状态下预测将来请求分布从而优化性能的目的。
有兴趣的同窗们能够在电梯单元结束以后尝试一下,(反正我懒,摸了)。
六.多电梯优化策略
1.全拆分
通过联合测试,发现原本指望度极高的图算法拆分在大量随机数据面前却败下阵来。缘由估计是电梯运行环境太过于复杂,换乘仅进行一次拆分的图算法虽然自己没有任何问题,但仍是难以预料电梯到底是怎么走的。但本次强测孰优孰劣却是很差说,数据点不少是[0.0]或者其余时间点集中投放,或者间隔极短投放,这时电梯处于起步状态,运行是可预料的,单一拆分就会有优点。
通过询问,采用下面这种方法的同窗也都所有91+,不失为一种不错的策略,而我,本次做业换乘只进行了一次盲目自信的豪赌拆分,并没拿到很高的优化分,哭辽。
①若是无需换乘,这我的把A, B, C电梯都摁一遍
②若是须要换乘,先尽可能在相同方向上拆分请求,但全部电梯组合(A&B、B&A、A&C、C&A、B&C、C&B)以及每种电梯组合的所有拆分可能都拆一遍,而且所有投入电梯,达到最混沌的状态。
好比请求1-FROM--3-TO-2对于A,B电梯组合会被分红: 1-FROM--3-TO--2(A) + 1-FROM--2-TO-2(B) 1-FROM--3-TO--1(A) + 1-FROM--1-TO-2(B) 1-FROM--3-TO-1(A) + 1-FROM-1-TO-2(B) 所有加进请求队列
//A,B电梯的上行请求拆分为例 floorsS = floorA floorsE = floorB; if (judgeIn(floorsS, fromFloor) & judgeIn(floorsE, toFloor)) { for (int p = 0; p < floorsS.length; p++) { for (int q = 0; q < floorsE.length; q++) { if (floorsS[p] == floorsE[q] && floorsS[p] > fromFloor && floorsE[q] < toFloor) { int[] req1 = {id, fromFloor, floorsS[p], i, j, 1}; int[] req2 = {id, floorsE[q], toFloor, j, j, 0}; request.add(req1); request.add(req2); flag = true; } } } }
③若是没法同向换乘,则反向拆分到距离请求楼层最近的楼层。
④一个电梯拿到请求,须要把其余电梯请求队列中id相同且非下一阶段的请求remove掉。
全拆分自己并非一种强大的算法优化,而只是一种减小平均损失的折中策略,它之因此能在强测中占重要一席之地,是由于电梯运行的随机性和数据点的随机性形成各种算法运行时间的不稳定性,甚至单一拆分的电梯抢人的运行时间仍然不够稳定,减少平均损失的折中可能不会拿到顶级分,但也会拿到很不错的优化分(91+)。
2.请求分配策略
原则:①不彻底平摊。快的电梯仍是应该多拿请求的,毕竟运行速度摆在那,但决不能鸽(🕊)了电梯C。
②不集中。也尽量不能让电梯一次吃足。
为何要提出这样的问题?
下面分析这样一个情景:
假设在1楼,B电梯吃饱了请求,C可能只吃了不多的请求。
B电梯拍拍肚子往2楼跑,这时有一个id=2333的人忽然怼门
2333-FROM-2-TO-3
A:不去
C:不去
B:饱了不去,等下波吧
id=2333的人:当场暴毙
若是B电梯把C电梯可共享的请求多分担一部分,就能够搭上这个请求了。
因此说,一个合理的调度,除了换乘以外,应当是电梯速度+负载状态的选择。
七.总结
2019年OO电梯单元的设计可谓独出心裁,我在其中获得了诸多历练,以上的优化点只是九牛一毛,更多的但愿你们不吝分享。总之:架构重要,架构重要,架构重要(哭唧唧)。