301
社区成员
发帖
与我相关
我的任务
分享目录
在本单元的三次作业中,我主要使用了synchronized关键词来上锁以保证线程安全。具体而言:
第一次作业比较简单,乘客请求指定了由哪部电梯来接,因此电梯的线程安全问题也相对来讲比较容易。由于本次作业中的共享对象只有请求和请求队列,所以我在本次作业中借鉴了第一次训练的做法,只在RequestQueue类中给除构造方法之外的所有方法用synchronized关键词修饰。如以下方法所示:
public synchronized void addRequest(Request r) {
requests.add(r);
notifyAll();
}
在第二次作业中由于加入了reset请求,所以我将调度器线程设置为在所有电梯都处理完各自的请求且请求总队列已结束的情况下再退出,以防调度器提前退出而无法处理因reset而需重新分配的请求。这就使得我在某些情况下需要调度器wait来避免轮询,在此我采用了用synchronized修饰代码块的方法:
synchronized (waitingLists.get(i)) {
try {
waitingLists.get(i).wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
其中waitingList.get(i)为第i部电梯自己的请求分队列,对应的电梯处理完自己的所有请求后会用notifyAll来唤醒调度器。
除此之外,我还在电梯类中新增了一些获取以及更改电梯属性的方法,并将这些方法用synchronized关键词修饰。
HW7中新增的主要是使得双轿厢电梯不会相撞的锁,我采用的方法是在电梯类里加一个属性:
private Object lock;
然后用synchronized(lock)将双轿厢电梯进入或离开换乘楼层的操作括起来,这样即可实现需求。
1、调度器设计
在本次作业中,因为一个乘客请求只能由指定的电梯去接,所以调度器只需要根据请求中的电梯编号将该请求发送到对应的电梯的请求队列即可。
调度器通过从总请求队列依次获取请求来与输入线程进行交互,并通过将获取到的请求发送到对应电梯的请求队列来与各电梯线程进行交互。
2、调度策略
本次作业中调度器不需要特殊的调度策略,只需根据请求中的电梯编号发送即可。至于每部电梯的运行策略,我采用的是LOOK算法,即电梯中没有人时判断哪一层有请求并去接,电梯中有人时根据乘客的需求决定电梯的运行方向,在电梯运行时只捎带与电梯运行同方向的乘客。
3、架构分析
UML类图如下:

UML协作图如下:
在本次作业中我的架构主要为输入线程->调度器线程->电梯线程,并且在后两次作业中也没有发生太大变化。这三个线程通过乘客请求串联在一起,像一条流水线一样完成对乘客请求的处理。输入线程将请求加入总请求队列,调度器从总请求队列中取出请求后分发到各电梯的分请求队列中,电梯再从各自的分请求队列中取出请求进行处理。
1、调度器设计
这次作业的调度器设计与上次基本相同。不过由于此次有重置请求,因此我将调度器线程改为了在总请求队列结束且为空以及所有电梯都完成了各自的请求之后再退出。
2、调度策略
本次作业不再指定电梯,所以需要有自己的调度策略。我采用的调度策略是算分法,即在要决定分发一个请求时,给六部电梯分别进行算分,然后将请求分配给分数最低的电梯。具体而言,我根据电梯当时的运行状态(如运行方向)和各项参数(如运行速度)以及乘客的起止楼层大致分了几种情况,每种情况估算出一个大概的参数(值越小越优先),然后将该参数与电梯的运行速度以及电梯当前所在楼层与乘客出发楼层的距离相乘得到最后的分数。
这样一来,通过将速度与楼层差相乘,我便可大致实现乘客等待时间与电梯耗电量的良好适应,而强测结果也证明确实如此。当然,除了以上提到的以外,我还考虑了其他的一些因素比如电梯是否在重置,电梯是否满员等。综合考虑下来,各项性能指标我基本都能做得很好。
而每部电梯的运行策略我仍然采用的是LOOK算法。
3、架构分析
UML类图如下:

UML协作图如下:
本次作业的架构与上次几乎没有发生变化,仍然是流水线式处理请求。不同之处主要在于在电梯接到reset请求后会把当前没有处理完的请求重新发送到总请求队列,需要调度器再次进行分配。
1、调度器设计
这次作业的调度器设计与第6次作业基本一样,只不过给调度器wait操作上锁的对象有所改变。
2、调度策略
由于本次作业出现了双轿厢电梯不方便算分,加之各种线程安全问题变得更加复杂,所以我在这次作业中放弃了算分策略,而是采用了均分策略,即用人数模6得到目标电梯的编号。当然了,这种算法策略只能保证正确性,在大部分情况下性能很一般。
3、架构分析
UML类图如下:

UML协作图如下:
本次作业的架构仍然没有发生太大变化,因此不再赘述。
在第三次作业中,我在电梯类中新加了两个属性lock和inChangeFloor,其中lock用于上锁,inChangeFloor则是用于判断电梯是否在换乘楼层。
在双轿厢电梯准备去往换乘楼层时,我会先让电梯进行判断,看另一个轿厢是否在换乘楼层或者还没有完全离开换乘楼层,如果是则等待,如果不是则将inChangeFloor置1,并开始向换乘楼层移动。其中上锁部分如下:
synchronized (lock) {
if (another.inChangeFloor()) {
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
synchronized (this) {
inChangeFloor = 1;
}
}
在电梯即将离开换乘楼层时(即输出ARRIVE之前),我会将inChangeFloor置为0,并用notifyAll将可能正在等待中的另一个轿厢唤醒。
此外,如果电梯在换乘楼层开关门之后,没有接到新的请求,我会让其自动移动一层以离开换乘楼层。
由此,便可以实现避免双轿厢电梯的碰撞。
在第一、二次作业中,我课下出现的主要bug是轮询。第一次作业主要是因为我的电梯线程在处理完所有的请求且请求队列还未结束之时并没有wait,而是毫无意义地空转,导致CPU资源的浪费。而第二次作业则是因为我的调度器在分发完所有的请求且电梯还没有全部运行完时没有wait,也造成了轮询。除此之外,我在强测和互测中均未出现错误。
第三次作业中的bug主要是死锁以及双轿厢电梯碰撞问题。死锁主要是因为我的代码中有一些需要上锁的地方没有把所有的情况都考虑到,导致在某些时候会出现正在wait的线程无法被唤醒的情况。
双轿厢电梯碰撞则是因为我在用synchronized(lock)将判断碰撞相关的代码块括起来时没有括完全,从而可能会出现一个轿厢在修改inChangeFloor(轿厢是否在换乘楼层)属性为1之前另一个轿厢就读取了其inChangeFloor的值而判断错误的情况,进而导致两个轿厢都向换乘楼层移动,导致碰撞,这也是我强测唯一错的一个点的原因所在。
面对多线程程序的debug方法:
当然,还有一点很重要,那就是耐心地重复运行错误样例,因为有些bug不容易复现(我曾经在本地跑一个错误样例跑了十几次才出错)。
早有耳闻的电梯月终于结束了,回过头来看,这三次作业相比于第一单元确实不太好写。但是经过这一个月的学习,我对于多线程的线程安全和层次化设计有了很大的收获。
首先是线程安全。在多线程中,如果涉及到对共享资源的访问或修改,那么线程安全就十分重要。在本单元中,保证线程安全主要是通过加锁的方法,一种是用synchronized关键词修饰代码块或方法,另一种是使用更为灵活的读写锁。不过这两种方法的本质都差不多,都是保证在同一时刻内不会出现对共享资源既读又写的情况。只要锁加得合理,那么线程安全问题就可以得到解决。
至于层次化设计,本单元中我的代码架构的主体是输入线程->调度器线程->电梯线程,根据乘客请求形成了一个流水线式的架构。这样的架构层次比较分明,逻辑上也很清晰。总之,通过本单元的学习,我对面向对象编程的层次化设计有了更深入的了解。