301
社区成员
发帖
与我相关
我的任务
分享在第二单元中,我们使用java多线程实现电梯调度。本单元三次作业难度不低,不仅要正确的实现多线程间的协作,还要考虑到调度策略的整体性能。每次的作业都是不小的挑战,但同时也提供了不可多得的练习机会。参考第三次课上实验的架构,我在本单元的三次作业开发较为顺利,并且没有进行大的架构调整。如今第二单元结束,借这次的博客总结,现将本人三次作业的代码架构进行详细分析,并记录本人的学习体会心得。
本次作业的目标是模拟多线程实时电梯系统,熟悉线程的创建、运行等基本操作,熟悉多线程程序的设计方法。
楼座内有六部电梯,电梯可以在楼座内1-11层之间运行。第一次作业指定了接送乘客的电梯
本次作业不涉及调度策略的选择,所有乘客都指定了电梯,所以只要处理好电梯线程Elevator和分配乘客的线程Schedule的共有变量 乘客等待队列processingQueue的锁,防止出现线程安全问题即可。
本次作业的架构是参照实验提供的代码。主要类有三个继承了Thread类的InputHandler,Schedule,Elevator和储存乘客信息的RequestQueue。
采用生产者消费者模型与工厂模式,InputHandler读取输入,产生请求传到总等待队列waitQueue,Schedule将waitQueue再处理,分发到各个电梯的等待队列RequestQueue,再由Elevator去处理这些请求。
代码架构的UML类图如下:

无,接近满分。猜测在电梯开关门的时机上可以做文章,但不见得修改之后平均性能会更好。
第六次作业在第五次作业的基础上增加了电梯重置功能,并且不再指定乘客乘坐的电梯。这样一来,就有很多的调度策略可以选择,特别是可以设置电梯的速度和容量,影响性能的参数就更多了。
本次的代码结构与第五次差别不大,主要在于Elevator增加了Reset方法,Schedule中增加了分配乘客的方法。
对于电梯重置的处理,先通过InputHandler分析出Reset指令,异步地将将被重置的电梯中的重置标志设置为有效,在电梯的run方法的循环中每次检查该标志是否有效,如果有效则进入重置流程,将所有乘客放出,如果乘客未到目的地则新增从下电梯楼层到乘客目的楼层的新请求,放回乘客的等待队列中去。
在处理电梯的重置时,遇到了一个bug,由于第五次作业Schedule线程结束的标志是读入指令结束并且乘客等待队列为空,但在本次作业中电梯重置时也会向乘客等待队列中新增请求,导致Schedule无法处理,于是我简单地将电梯重置放出的乘客直接放回该电梯的队列中,跳过了Schedule的分配过程,实际上这样的分配效果还是不错的,毕竟电梯就在当前楼层。另一种处理方法是以请求全部完成为Schedule线程结束的标志。
对于分配策略的选择,有很多选择,如影子电梯,调参法等等。由于影子电梯实现较为复杂,考虑到开发时间的问题,我简单的计算每部电梯到该请求所在楼层的时间,并选择最少的那个。分配策略有一些需要注意的点:
由于考虑不足,在互测阶段被特殊点hack成筛子了。

第六次作业中,强测接近满分,但互测被一些特殊的数据hack出TLE了。由于分配策略的漏洞,在同一时刻的大量请求会被分配到同一电梯中,导致超时。于是给分配的数量设置了上限,并给时间加上了一定的随机数,让策略的选择有一定的不确定性,防止被特殊数据hack。
第七次作业在第六次作业的基础上增加了双轿厢电梯,双轿厢电梯不能同时停靠换乘楼层,这给电梯调度产生了额外的复杂度。我的实现不太适合拓展双轿厢电梯,于是修改了较多的代码。双轿厢电梯的重置可沿用普通的重置代码,并额外设置双轿厢的标志,在reset时选择是普通的重置还是分裂重置。
为了实现双轿厢电梯,我在初始化时就创立12个电梯线程和12个队列,并两两配对,分为AB电梯,但是正常情况下只有A电梯运行,而B电梯不启动,也不会给它分配请求。A电梯就相当于之前的普通电梯,只有在分裂请求到来时,A电梯才会启动B电梯,并初始化A、B电梯的各种参数。
为了实现换乘效果,对于双轿厢电梯,当到达换乘楼层时,轿厢内的所有乘客都会下电梯,如果乘客仍未到达目的地,就新增从换乘楼层到目的楼层的请求,放入等待队列,类似先前的实现。
对于停靠楼层的限制,我新增了一个锁类来保证单一电梯在换乘楼层。电梯想要进入换乘楼层则必须获得该锁,否则进入等待,直到锁被放开。
//Lock.java
public class Lock {
private boolean isLocked = false;
public synchronized void lock() {
while (isLocked) {
try {
wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
isLocked = true;
}
public synchronized void unlock() {
isLocked = false;
notifyAll();
}
}
代码架构的UML类图如下:

由于外出比赛,本次作业没有及时完成,没有参与互测。强测中有概率出现线程无法结束的问题,是因为双轿厢电梯其中一部已经结束,而另一部还有需要转乘的乘客,于是乘客就卡住换乘楼层没有电梯来接了,只要在电梯线程结束的判断上加上伙伴电梯也无任务时才结束。
public synchronized PersonRequest getOneRequestAndRemove() {
if (requests.isEmpty()) {
eWait();
}
if (requests.isEmpty()) {
return null;
}
PersonRequest request = requests.get(0);
requests.remove(0);
this.notifyAll();
return request;
}
使用synchronized修饰方法getOneRequestAndRemove,确保同一时间只有一个线程可以访问该方法,以达到同一时间只有一个线程能修改request,保证线程安全。
第五次作业中,调度器只是一一对应,将对应请求通过共享对象RequestQueue分给对应的电梯的等待队列。
后两次作业,就简单采用计算每部电梯到达请求楼层所需的时间来分配,但是问题比较大,为了各种特殊点修修补补,比如给这个时间加上一定的随机量,对在重置中的电梯不分配,给每部电梯设置请求数上限。至于耗电量之类的参数,感觉花大把时间调参,去争取那一点点的分数很不值,就完全没管。
三次的线程结构没有太大的变化,仅仅是增量开发,连类就只增加了一个简单的锁类。就直接给出第七次作业的UML协作图




双轿厢的两个轿厢不碰撞的逻辑,我已在第七次作业的分析中提到,用锁来保证最多只有一个电梯能在换乘层,并写定AB电梯不能停留在换乘层,无论是否需要移动,都移动到换乘层的相邻层。
主要的bug还是集中在调度策略的不完善与线程间死锁的问题,之前的每次作业中也提到了。这些同时也是本单元的难点。调度策略尚能通过缝缝补补的方式通过,大不了随机分配摆烂也能得到还不错的性能分,但是线程问题的bug就很折磨了,debug难度很大。
对于debug方法,由于多线程使用常规打断点的方式会出现不同的表现,所以我主要还是通过print方法打印出一部分信息,帮助我发现了很多bug。另一个方法就是在IDEA中,如果程序死锁卡住了,可以点击转储线程,固定住当前线程的栈,分析这些线程都卡在什么位置了,通常就能发现死锁的位置。
这一单元的作业难度较大,但我很喜欢这种挑战,从零开始构建一个多线程的系统。这个过程中,我学到了很多多线程的知识,对于多线程的协作、调度有了更深的理解。虽然在开发过程中遇到了很多困难,但是解决问题的过程也让我得到了很大的成就感。特别是在比赛的压力下,压缩开发时间也让我学会了更高效的开发方法,学会了权衡取舍,学会了如何在有限的时间(大约是每次作业4小时内)内完成一个复杂的系统。