1.电梯采用LOOK算法,这是对SCAN算法的改进。假设现在电梯正在上行,到达某一层停靠后电梯轿厢里没有人并且当前楼层到顶层没有请求,则转向。这样就省去了从当前楼层到顶层,和从顶层回来的这一段折返的空载的路程(或者说载客可能性不大的的路程,因为不能保证转向后没有新的请求到来,但是综合考虑还是转向的运行时间期望更小,受益更大)。
具体判断方法:
1 if (requests.isEmpty() && canPark()) { 2 if (!dispatcher.checkFurtherRequest(currentFloor, dir)) { 3 if (isOpen) { 4 closeDoor(); 5 } 6 changeDir(); 7 } 8 }
其中checkFurtherRequest为:
1 public synchronized boolean checkFurtherRequest( 2 int currentFloor, ElevatorDirection dir) { 3 if (dir == ElevatorDirection.UP) { 4 for (int i = 0; i < requestQueue.size(); i++) { 5 if (requestQueue.getFrom(i) > currentFloor) { 6 return true; 7 } 8 } 9 return false; 10 } else { 11 for (int i = 0; i < requestQueue.size(); i++) { 12 if (requestQueue.getFrom(i) < currentFloor) { 13 return true; 14 } 15 } 16 return false; 17 } 18 }
其他函数根据名字可以大体推知其作用。
2.为了最大限度的不错过可以执行的请求,可以在关门前先遍历一遍请求列表,没有可执行的请求就sleep(200),之后再遍历一遍请求列表,若此时有可执行的请求就获取它并继续sleep(200)重复上述过程,若此时仍然没有可执行的请求就关门。虽然只有0.2s的差别,但是这样就可能让你不错过诸如下列的一系列请求:
(虽然可能性极低,但实测确实有可能发生)
3.一开始我曾构思过写一个调度器线程,把请求动态分给三个电梯,如果有一个电梯不能完成的请求就动态分配中间停靠楼层,但这样需要线程之间频繁的通信,实现起来太过复杂,而且因为三个电梯的运行时间、限乘人数都不一样,要综合各种因素实现一个稳定的优化算法是极为困难的,且留给我们的时间也不允许。于是我的做法就是化繁为简,以不变应万变。找到了一个低付出高回报的方法。
a.既然动态分配中间停靠楼层不好实现,就把一个电梯不能完成的所有请求枚举出来,固定他们的中间停靠楼层。我的具体实现如下表:
|
不能简简单单直接将请求拆成两部分放在请求列表里,这样可能会有后半段先执行的逻辑错误,我的方法是队请求做出特殊的标记
public class Request { private int id; private int from; private int to; private int destiny; private boolean in = false; private boolean out = false; public Request(int id, int from, int to) { this.id = id; this.from = from; this.to = to; destiny = 0; } public Request(int id, int from, int to, int destiny) { this.id = id; this.from = from; this.to = to; this.destiny = destiny; } //此处省略部分get和set方法 }
设计了两种构造方法,单电梯可直达的用第一种构造方法,不能执行的用第二种构造方法,其中from不变,to为中转的楼层,destiny为最终的目的楼层。每次执行完一条请求后看看这个请求的destiny是否为0,若为0,该请求已经执行完成,删去;若不为0,则重新构造一个请求:
request = new Request(formerRequest.get(i).getId(), currentFloor, formerRequest.get(i).getDestiny());
以当前楼层为from,destiny为to,并立即插入请求列表中等待再一次被执行,这样可以保证逻辑的先后顺序不会出错。
b.既然动态给三个电梯分配请求太困难,就让三个电梯来请求列表里“抢”请求,但这个“抢”是有约束的。电梯运行到某层后遍历请求列表,取得电梯自己可以执行的请求。什么是电梯自己可以执行的请求呢?就是currentFloor == requestFrom并且方向与电梯运行方向相同的请求。电梯每次到达某一层之后,先下人,然后再遍历请求列表来取得请求。
这就是我们常说的Worker Thread模式,其类图如下:
可以对比一下Worker Thread模式和普通的方法调用。
在Worker Thread模式中,Client负责发送工作请求,它将工作内容封装为Request,然后传递给Channel,在普通的方法调用中,这部分相当于“设置参数并调用方法”。其中,“设置参数”与“创建Request”相对应,“调用方法”与“传递给Channel”相对应。
Worker负责进行工作,它使用从Channel接受到的Request来执行实际的处理。在普通的方法调用中,这部分相当于“执行方法”。
在进行普通的方法调用时,“调用方法”和“执行方法”是连续进行的。因为调用方法后,方法会立即执行,无法分开。但在Worker Thread模式中,方法调用和方法执行被特意被分开了。
这种分离有什么意义呢?
①提高响应速度
如果执行和调用不可分离,那么当执行需要花费很长时间时,就会拖调用处理的后腿。但是如果将调用和执行分离,那么即使执行需要花费很长时间也没有关系,因为执行完调用处理的一方可以先继续执行其他处理,这样就可以提高响应速度
②控制执行顺序,即调度
如果调用和执行不可分离,那么在调用之后就必须开始执行。但如果将调用和执行分离,那么执行就可以不再受调用顺序的制约。我们可以通过设置Request的优先级,即控制Channel将Request传递给Worker的顺序来实现控制调用顺序,这就是请求调度(scheduling)。
③可以取消和反复执行
将调用和执行分离后,还可以实现“即使调用了也不执行”和“即使调用了一次也可以反复执行”。
④实现分布式
我们现在只是分开不同的线程来实现调用和执行的分离,但是利用这种思想,我们可以将负责调用的计算机和负责执行的计算机分开,让网络作为Channel来传递Request。
4.没有最好的优化,只有最好的架构,不能为了性能而破坏设计的架构,这样是得不偿失的,性能只是为你的架构锦上添花,不能为了性能而使架构变得不堪入目,本末倒置。对电梯类的建造我采用了抽象工厂模式生成了三个电梯类。其中抽象工厂为:
1 public interface ElevatorFactory { 2 3 static final int OPEN_TIME = 200; 4 static final int CLOSE_TIME = 200; 5 6 public abstract void runElevator(); 7 8 public abstract void walkIn(Request request); 9 10 public abstract void walkOut(Request request); 11 12 public abstract void openDoor(); 13 14 public abstract void closeDoor(); 15 16 public abstract void moveOneFloor(int sign); 17 18 public abstract int getSign(); 19 20 public abstract void changeDir(); 21 22 public abstract boolean canPark(); 23 }
我的做法是创建了三个不同的电梯类,分别实现这个接口,这样会造成类的数目过多且三个类之间的相似度特别高。其实可以创建一个电梯类并实现这个接口,然后把三个电梯之间不同的部分通过参数传进来,实现一个类的三个不同对象即可,这一点是需要改进的地方。
以上几点只是个人抛砖引玉的愚见,一定有不周和不对之处,望读者多多包涵并批评指正。