hw5
UML类图
整体架构
第一次作业采用InputThread-Schedule-Elevator架构,从InputThread中读入乘客请求,再由Schedule分配给Elevator。每一部Elevator独立拥有自己的请求列表与一个策略类Strategy,利用Strategy根据当前请求列表中的内容发出运行指令。
锁的选择
在Main类中启动包括五部电梯、Schedule、InputThread在内的7条线程,将共享变量mainTable传入Schedule和InputThread,并使用synchronized锁对其进行保护,保证读取请求与分配请求的操作不能同时发生。
调度策略
本次作业采用LOOK算法,具体为:
- 首先为电梯规定一个初始方向,然后电梯开始沿着该方向运动
- 到达某楼层时,首先判断是否需要开门
- 如果发现电梯里有人可以出电梯(到达目的地),则开门让乘客出去
- 如果发现该楼层中有人想上电梯,并且目的地方向和电梯方向相同,则开门让这个乘客进入
- 进一步判断电梯里是否有人。如果电梯里还有人,则沿着当前方向移动到下一层;否则,检查请求队列中是否还有请求
- 如果请求队列不为空,且某请求的发出地是电梯运行方向上的某楼层,则电梯继续沿着原来的方向运动
- 如果请求队列不为空,且所有请求的发出地都在电梯运行方向后方的楼层上,或者是在该楼层有请求但是这个请求的目的地在电梯后方,则电梯掉头并进入判断是否需要开门的步骤
- 如果请求队列为空,且输入线程没有结束(即没有输入文件结束符),则电梯停在该楼层等待请求输入(wait)
- 如果请求队列为空,且输入线程已经结束,则电梯线程结束
hw6
UML类图
新增要求
新增了reset请求与Receive输出要求,并将乘客指定电梯改为电梯竞争乘客
整体架构
与第一次作业相同,由InputThread统一读取输入,再由Schedule分配请求。
对于RequestTable类,新增一个ResetRequest类型的变量,用于存放reset请求;同时设置一个布尔值hasReset,用于判断当前列表里是否存在需要处理的reset请求。
在测试过程中发现,在高并发时Schedule对于reset请求的分配具有比较明显的延迟,于是将reset请求的分配工作转到InputThread,Schedule只负责分配乘客请求。
电梯调度策略以及同步块的设置与上一次作业相同。
电梯分配策略
总体策略是将乘客分配给当前最“闲”的电梯,即均匀分配。具体实现如下:
- 遍历六部电梯,若当前电梯正在等待且不处于reset状态,则直接分配
- 如果找不到这样的电梯,则遍历电梯并计算电梯请求数量与内部人数的和,找出负载最小的电梯
- 在计算负载之前,如果检测到当前电梯正在reset,则等待100毫秒并再次检查,直到电梯完成reset再进行计算,最终返回负载最小的电梯id
这样的分配策略实现方法较为简单,并且在请求数量特别大的情况下会有比较优秀的性能表现。
hw7
UML类图
UML协作图
新增要求
增加了新一类reset请求,将电梯重置为双轿厢电梯并设置一个换乘楼层,规定同一时刻只能有一个轿厢处于该楼层。
整体架构
本次作业相比第二次作业有较大改动。
对于双轿厢电梯,总体思路是让两个轿厢共享同一个requestTable并共同处理其中的乘客请求,并且对于换乘楼层上锁,保证两个轿厢不同时处于换乘楼层。
在Elevator内部新增一个Elevator类型的属性,使得轿厢A与轿厢B能够互相持有彼此,以此共享或请求列表并方便访问彼此的某些属性;除此之外还需要新增轿厢类型、换乘楼层等必要的属性。
修改move方法,在执行move操作前先检查下一楼层是否为换乘层,如果是,则调用attemptToEnterChangeFloor方法尝试进入换乘层。
在reset之前增加判断方法,确定reset请求的类型,分别处理不同类型的reset请求。
锁的选择
要实现对于换乘楼层的保护,锁的使用至关重要。在这里我使用了ReentrantLock,并且在电梯内部设置一个布尔值类型的状态变量floorOccupied,该变量在两部轿厢之间共享,用以判断换乘楼层是否被某个轿厢所占用。
当某一部轿厢检测到即将进入换乘楼层时,它会调用attemptToEnterChangeFloor方法,具体实现如下:
public void attemptToEnterChangeFloor(int nextFloor) {
floorLock.lock();
try {
// 如果想要进入的楼层是换乘层并且换乘层已被占用,则等待
while (nextFloor == transFloor && dcElevator.floorOccupied) {
floorCondition.await();
}
// 当前电梯可以进入换乘层
floorOccupied = true;
//进入换乘层
move();
//开门并进出乘客
openAndClose();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
floorLock.unlock();
}
}
这个方法保证如果另一部轿厢正在换乘楼层,则本轿厢会进入等待状态,直到换乘楼层不再被占用。并且在进入换乘楼层时更新floorOccupied为true,表明换乘楼层当前被自己占用。
在openAndClose方法中,如果检测到当前楼层为换乘层,则会在进出乘客完成后立即调用leaveChangeFloor方法离开换乘楼层,具体实现如下:
public void leaveChangeFloor() {
floorLock.lock();
try {
floorOccupied = false;
leave();//离开楼层
floorCondition.signalAll(); // 通知所有等待的电梯
} finally {
floorLock.unlock();
}
}
这个方法使得电梯在离开换乘楼层时先将floorOccupied更新为false,再移动一层离开换乘层,之后再通知正在等待进入换乘层的电梯,这样就保证了状态变量floorOccupied的正常更新,进而保证了两部轿厢不会在换乘层撞车。
bug修复与心得体会
遇到的bug
第一次作业并没有遇到比较大的bug。
第二次与第三次作业遇到的bug主要是分配策略的问题。在第二次作业中,如果检测到当前电梯正在重置,则跳过这部电梯。这会导致当六部电梯不同步重置,并且在重置期间遇到大量请求同时输入时,Schedule会把所有请求分配给不处于重置状态的少数电梯,导致其他电梯在重置完成后无事可做而只有一两部电梯在运行,最终运行时间过长。
解决办法如下:
while (elevator.ifReset()) {
try {
sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
若检测到电梯正在重置,则等待100毫秒后重复检测,直到电梯完成重置。这样修改后请求会分配的更加均匀,提升了电梯性能。
bug修复方法
对于多线程的程序,调试是一件非常困难的事情。在一遍又一遍的折磨后,我发现了一个IEAD自带的功能:
就是终端里这个照相机的图标,在程序运行到某一时刻时点击这个图标,可以捕获这一时刻所有正在运行的线程的状态:
左边的Thread-n是用户启动的线程。Thread后面的序号是按照线程启动的顺序编排的,在我的代码中六部电梯的线程是最先启动的,所以Elevator-1到Elevator-6对应的线程就是Thread-0到Thread-5,其他线程以此类推。
- 线程状态共有waitting,runnable和blocked三种。右边第二行"Thread.State: WAITTING"表明当前线程的状态是等待,如果程序无法结束而某些线程一直处于watting状态,则表明可能发生了死锁。
- runnable表示线程正在运行,如果代码无法正常结束而某个线程一直处于runnable状态,则大概率是发生了轮询。
- blocked表示线程在尝试持有某个锁时被阻塞,应该是同步块没有设置好。blocked比较少见。
通过上面的方法确定出错的线程后,就可以使用print大法了。假设确定id为1的电梯运行出错,就可以在电梯状态变化时(如进入等待、移动楼层等)进行打印输出,例如下面这个方法:
private void waitRequest() throws InterruptedException {
synchronized (requestTable) {
isWait = true;
if (id == 1) {
System.out.println("enter wait");
}
requestTable.wait(); // 如果为空,则电梯线程等待
if (id == 1) {
System.out.println("quit wait");
}
isWait = false;
}
}
这样就可以在不干扰输入时许的情况下监视某个线程的状态变化,从而找出bug所在。
心得体会
线程安全
线程是否安全主要取决于锁的使用。需要上锁的共享变量主要涉及到总的请求列表、电梯自己的请求列表,以及第三次作业涉及到的换乘楼层。在上锁时需要注意死锁的问题,并且根据不同的情况选择不同的锁。
层次化设计
参考了往届学长的博客以及实验代码,从第一次作业就开始使用InputThread-Schedule-Elevator的架构层次,各个线程各司其职、互相合作,高效地处理请求,并且拥有比较高的拓展性,因此三次作业都能够在这个架构的基础上完成,大大提升了效率。