一、同步块的设置和锁的选择
第一次作业中,同步块仅出现在类RequestQueue中,这个类存放共享数据,其实例对象有两个,一个是总的等待队列waitQueue,另外还有每个电梯的等待队列elevatorQueue,同步块用sychronize加锁。
共有4种进程,分别是主进程main,输入进程InputThread,控制器进程Controller和每个电梯的进程Elevator,输入进程和控制器进程共享waitQueue这个总的等待队列,因为waitQueue的每个方法都被sychronize加锁,所以同步块中的语句每次只能被一个进程执行,waitQueue中储存的等待的人的增减就保证了线程安全。
在第二次作业中,由于需要增减电梯,所以我又定义了一个电梯队列类,这也是一个需要共享的共享数据,所以它的每个方法我也都用了sychronize加锁,这样位于同步块中的语句每次也就只能被一个进程访问。
第三次作业中出现的同步块和第二次作业几乎相同,只是在某些非共享数据类中为了解决一些特殊的情况,增加了方便notifyAll的同步块,但是这种写法使得代码的耦合性增加,不便于理解和阅读,更不便于迭代。
同步块中的的处理语句在这几次作业中主要包括wait和notifyAll,因为一个共享数据可以被多个线程访问修改,所以,可能某个线程对共享数据的修改依赖于其他线程对该数据的修改,所以当这个条件不满足的时候,这个线程就要wait,让出资源,让其他进程先执行,当其他线程改变了这个数据之后,才能想办法让这个进程继续执行,这个想办法就是通知(notifyAll)。
在控制块中,当当前数据不满足条件时,让进程wait等待,当修改数据之后,执行notifyAll方法,通知那些处于wait状态的进程继续执行。这些就是锁和同步块中语句的作用,以及在本单元作业中,我都锁和同步块的应用。
二、调度器及调度策略的迭代分析
三次作业中的调度器均为Controller这个类。这个类只需要将waitQueue队列中等待的人按照一定的策略分配到每个电梯的等待队列中去就可以了。
调度策略的迭代过程在前两次作业都比较简单,第一次作业因为不涉及电梯的增减,直接按照平均分配原则,将所有的人平均分配到每个电梯中去就可以了,代码如下:
PersonRequest request = waitQueue.getOneRequest();
if (request == null) {
continue;
}
//---------对waitQueue进行调度---------
elevatorQueues.get(time % 6).addRequest(request);
time++;
第二次作业仅仅只是增加了电梯的增减,在添加了电梯队列,实时掌握电梯数量之后,也是可以采用相对平均的方法进行调度,代码如下:
Person request;
request = waitQueue.getOneRequest();
if (request == null) {
continue;
}
//---------对waitQueue进行调度---------
int index = time % elevators.size();
while (elevators.getElevatorLine().get(index).getMaintain() == 1) {
index = (index + 1) % elevators.size();
}
elevators.get(index).getEPersonQueue().addRequest(request);
time++;
但是这种调度策略在强测时出现了很多的RTLE,所以我在debug的时候又采用了相对随机的方式进行调度,代码如下:
Person request;
request = waitQueue.getOneRequest();
if (request == null) {
continue;
}
//---------对waitQueue进行调度---------
Random random = new Random();
index = random.nextInt(elevators.size());
while (elevators.getElevatorLine().get(index).getMaintain() == 1) {
index = random.nextInt(elevators.size());
}
elevators.get(index).getEPersonQueue().addRequest(request);
time++;
第三次作业由于增加了换乘,所以调度策略相对复杂,换乘的核心思想是一个递归的算法,目的是为了找到所有能够到达目标楼层的换乘次数最少的线路,为了实现这个功能,我在第三次作业中相对第二次作业增加了一个叫做Strategy的类,这个类中存了换乘的次数,每次换乘的第一部电梯,第一次需要到达的楼层等等,具体代码如下:
public class Strategy {
private ArrayList<Integer> eleIdList;
private ArrayList<Integer> toFloorList;
private Integer changeTime;
private Integer floorDiff;
}
int fromFloor = request.getFromFloor();
int toFloor = request.getToFloor();
int eleID = 0;
request.setThisFrom(fromFloor);
//TODO 此处为主要策略
finalStrategy = new Strategy();
finalStrategy.setChangeTime(20);
synchronized (elevators) {
Collections.shuffle(elevators.getElevatorLine());
findBegin(fromFloor, toFloor);
eleID = finalStrategy.getFirstEleId();
request.setThisTo(finalStrategy.getFirstToFloor());
elevators.getEleOfId(eleID).getEPersonQueue().addRequest(request);
elevators.notifyAll();
}
//findBegin和它调用的find是两个递归函数
三、从类图看三次作业架构设计及未来扩展能力
第一次作业的设计比较简单,输入、控制、电梯各为一个线程,输入通过控制器把人分配给每个电梯,电梯从自己的等待队列中找人,按照ALS算法实现捎带,完成上下人、开关门的操作。
第二次作业在第一次作业的基础上仅仅是增加了电梯队列,用来实现电梯的增减。
第三次作业与第二次作业的主要框架相同,唯一的区别在于控制类调度策略的不同,为了能更方便地实现这个策略,新增了一个策略类。
在未来的扩展中,主要更改的部分还是调度类的调度策略,同时也需要根据不同的调度策略,适时改变电梯运行过程中的输入输出。另外,也可以针对性地优化每个电梯的捎带策略,以提高电梯的运行效率。
四、从UML协作图看线程之间的协作关系
主进程创建了三类进程:电梯进程、控制器进程、输入进程。输入进程和控制器进程的联系在于等待队列,控制器和电梯进程的关联是每个电梯的队列。简单来说,只有输入进程输入了,控制器才能进行分配,没有输入了,电梯和控制器才能结束。
五、三次作业的稳定内容和易变内容
稳定内容:三次作业从输出到总的等待序列waitQueue的过程是不变的,每个电梯的运行过程也是几乎不变的,尤其是在上下人,开关门的操作上。
易变内容:针对不同的需求,电梯的调度策略会有明显的不同,同时,对于每次的特殊要求,电梯在运行过程中可能会需要进行不同的特判和操作,比如,第二次作业,在输入告诉电梯需要维护时,电梯需要开门下人,第三次作业电梯不能无条件开门等,这些都是容易发生变化的内容。
六、bug分析及debug方法
第一次:取出队列中的请求时没有考虑电梯中的人数,导致当人数为6时,人从队列中取出但是没有进入电梯,凭空消失,代码如下:
//更改前,如果此时电梯人数为6,判断是否要有人进电梯时,会先把人从电梯的请求队列中取出,但是由于电梯已满,没有把人装进电梯,此时会导致此人消失。
private void findInPerson() {
PersonRequest request = null;
request = elevatorQueue.getOneRequestOfFloor(this.floor, this.status);
while (request != null && this.numOfPeople < 6) {
addRequest(request);
if (this.doorStatus == Defines.DOOR_CLOSE) {
openDoor(this.floor);
}
personIn(request.getPersonId(), this.floor, this.elevatorID);
if (numOfPeople < 6) {
request = elevatorQueue.getOneRequestOfFloor(this.floor, this.status);
}
}
if (this.doorStatus == Defines.DOOR_OPEN) {
closeDoor(this.floor);
}
}
//改后,在取人之前先判断电梯中人数是否小于6,若电梯已满,则不会从请求队列中取人。
private void findInPerson() {
PersonRequest request = null;
if (this.numOfPeople < 6) {
request = elevatorQueue.getOneRequestOfFloor(this.floor, this.status);
}
while (request != null && this.numOfPeople < 6) {
addRequest(request);
if (this.doorStatus == Defines.DOOR_CLOSE) {
openDoor(this.floor);
}
personIn(request.getPersonId(), this.floor, this.elevatorID);
if (numOfPeople < 6) {
request = elevatorQueue.getOneRequestOfFloor(this.floor, this.status);
}
}
if (this.doorStatus == Defines.DOOR_OPEN) {
closeDoor(this.floor);
}
}
第二次:答案没有错误,但出现了过多的RTLE,仔细看了一下,采用的分配策略确实是平均分配,而且每个电梯也都可以捎带,不知道为什么会TLE,最后稍微改动了一下分配策略,改成了随机分配,即用随机数随便指定一个队列中的电梯加进去,虽然具有很大的不确定性,但依然怀着“买彩票中大奖”的心态试了一下,虽然又时还是会有部分测试点TLE,但还是有能全部通过的时候,就这样,我水过了bug修复,不知道会不会有后遗症,代码如下:
Random random = new Random();
index = random.nextInt(elevators.size()); //在电梯数量范围内随机取一个整数
while (elevators.getElevatorLine().get(index).getMaintain() == 1) {
index = (index + 1) % elevators.size();
index = random.nextInt(elevators.size());
}
elevators.get(index).getEPersonQueue().addRequest(request);
第三次:第一个错在对于共享对象数据的一次访问没有加锁,导致出现了线程安全问题;另一个比较严重的问题是,我对于只接人电梯的判断时机出现问题,我的判断和进人是在一个函数中,对于某一个电梯来说,只要有人进就会开门,进完人才会判断该电梯是否是只进人,这就导致如果此时已经有两个电梯在只接人,那么该电梯依然会开门接人,出现错误,代码如下:
private boolean findInPerson() {
Person request = null;
boolean hasInPerson = false;
if (this.numOfPeople < maxPersonNum) {
request = elePersonQueue.getOneRequestOfFloor(this.floor, this.status);
}
while (request != null && this.numOfPeople < maxPersonNum) {
hasInPerson = true;
addRequest(request);
if (this.doorStatus == Defines.DOOR_CLOSE) {
this.onlyInPerson = 1;
openDoor(this.floor);
}
personIn(request.getPersonId(), this.floor, this.elevatorID);
if (numOfPeople < maxPersonNum) {
request = elePersonQueue.getOneRequestOfFloor(this.floor, this.status);
}
}
if (this.doorStatus == Defines.DOOR_OPEN) {
closeDoor(this.floor);
}
return hasInPerson;
}
//……
boolean hasOutPerson = findOutPerson();
if (maintain()) {
elevators.remove(this.elevatorID);
return true;
}
boolean hasInPerson = findInPerson();
if (hasOutPerson) {
this.onlyInPerson = 0;
} else if (hasInPerson) {
this.onlyInPerson = 1;
} else {
this.onlyInPerson = 0;
}
//……
Debug的方法主要是想办法复现Bug,将错位的stdin反复输入,通过输出中间量来观察具体的出错部位,虽然过程很繁琐,但是确实可以一步步找出问题的所在,在这个过程中,也可以加深对代码逻辑的理解。
七、心得体会
线程安全
线程安全问题不太容易理解,但总的目标就是对共享数据加锁,让共享对象在每个时刻只能被一个进程访问,特别注意放置死锁的问题,也就是说,不能所有的进程同时陷入等待状态,关键在于适时的notifyAll(尽量用notifyAll而不是notify)。
至于CPU轮询问题,没有一个很好的debug方法,只能在设计时多加考虑,当出现轮循时,主要检查哪些地方调用过含有wait方法的方法,判断是否会有可能出现一直等下去的情况。
层次化设计
层次化设计在这次的作业中主要体现在各个进程完成各自的事情,通过共享对象将他们联系在一起。这样的设计在迭代时,只需要考虑不同的部分,对于这个单元来说,控制器进程和电梯进程在每次迭代时变化最大,只要将这两个进程从其他部分解耦出来,就能比较方便地进行迭代。