BUAA-OO-24-Unit2总结

孙延范-22371521 学生 2024-04-19 22:12:03

第二单元总结


前言

在第二单元中,我们使用java多线程实现电梯调度。本单元三次作业难度不低,不仅要正确的实现多线程间的协作,还要考虑到调度策略的整体性能。每次的作业都是不小的挑战,但同时也提供了不可多得的练习机会。参考第三次课上实验的架构,我在本单元的三次作业开发较为顺利,并且没有进行大的架构调整。如今第二单元结束,借这次的博客总结,现将本人三次作业的代码架构进行详细分析,并记录本人的学习体会心得。


第五次作业

题目分析

本次作业的目标是模拟多线程实时电梯系统,熟悉线程的创建、运行等基本操作,熟悉多线程程序的设计方法。

楼座内有六部电梯,电梯可以在楼座内1-11层之间运行。第一次作业指定了接送乘客的电梯

本次作业不涉及调度策略的选择,所有乘客都指定了电梯,所以只要处理好电梯线程Elevator和分配乘客的线程Schedule的共有变量 乘客等待队列processingQueue的锁,防止出现线程安全问题即可。

代码分析

本次作业的架构是参照实验提供的代码。主要类有三个继承了Thread类的InputHandlerScheduleElevator和储存乘客信息的RequestQueue

  • InputHandler利用课程组提供的接口,处理输入,并根据传递给Schedule
  • Schedule采用一定的调度策略,将等待的乘客分配到某个电梯中的RequestQueue去,由于第一次作业指定了电梯,所以本次作业的调度策略就是直接按指定的id分配,但这个类对未来两次作业的实现至关重要。
  • Elevator实现电梯,根据Schedule分配的RequestQueue内的乘客请求进行处理。
  • RequestQueue由于会被Schedule和Elevator同时访问,所以对必要的方法都加了synchronized修饰,保证线程安全。

采用生产者消费者模型与工厂模式,InputHandler读取输入,产生请求传到总等待队列waitQueue,Schedule将waitQueue再处理,分发到各个电梯的等待队列RequestQueue,再由Elevator去处理这些请求。

UML类图

代码架构的UML类图如下:

img

强测与互测BUG分析

无,接近满分。猜测在电梯开关门的时机上可以做文章,但不见得修改之后平均性能会更好。

第六次作业

题目分析

第六次作业在第五次作业的基础上增加了电梯重置功能,并且不再指定乘客乘坐的电梯。这样一来,就有很多的调度策略可以选择,特别是可以设置电梯的速度和容量,影响性能的参数就更多了。

代码分析

本次的代码结构与第五次差别不大,主要在于Elevator增加了Reset方法,Schedule中增加了分配乘客的方法。

  • 电梯的重置

对于电梯重置的处理,先通过InputHandler分析出Reset指令,异步地将将被重置的电梯中的重置标志设置为有效,在电梯的run方法的循环中每次检查该标志是否有效,如果有效则进入重置流程,将所有乘客放出,如果乘客未到目的地则新增从下电梯楼层到乘客目的楼层的新请求,放回乘客的等待队列中去。

在处理电梯的重置时,遇到了一个bug,由于第五次作业Schedule线程结束的标志是读入指令结束并且乘客等待队列为空,但在本次作业中电梯重置时也会向乘客等待队列中新增请求,导致Schedule无法处理,于是我简单地将电梯重置放出的乘客直接放回该电梯的队列中,跳过了Schedule的分配过程,实际上这样的分配效果还是不错的,毕竟电梯就在当前楼层。另一种处理方法是以请求全部完成为Schedule线程结束的标志。

  • 分配策略

对于分配策略的选择,有很多选择,如影子电梯,调参法等等。由于影子电梯实现较为复杂,考虑到开发时间的问题,我简单的计算每部电梯到该请求所在楼层的时间,并选择最少的那个。分配策略有一些需要注意的点:

  • 当有电梯在重置时,是不能分配请求的,于是将计算时间函数的返回值设置成INT最大值。
  • 当五台电梯重置时,会把所有请求全分给一部电梯,导致分配不均,于是设定电梯等待队列的人数上限,如果等待人数过多也将时间函数的返回值设置成INT最大值。

由于考虑不足,在互测阶段被特殊点hack成筛子了。

UML类图

img

强测与互测BUG分析

第六次作业中,强测接近满分,但互测被一些特殊的数据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类图

代码架构的UML类图如下:

img

强测与互测BUG分析

由于外出比赛,本次作业没有及时完成,没有参与互测。强测中有概率出现线程无法结束的问题,是因为双轿厢电梯其中一部已经结束,而另一部还有需要转乘的乘客,于是乘客就卡住换乘楼层没有电梯来接了,只要在电梯线程结束的判断上加上伙伴电梯也无任务时才结束。

总结

同步块的设置

  1. 三次作业中我用到了JVM内置的sychronized关键字,举例如下:
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,保证线程安全。

  1. 第七次作业中我建立了Lock类来确保双轿厢电梯同时只有一部能在换乘层。也用到使用synchronized来确保同时只能有一个线程访问lock和unlock方法。

调度器设计

第五次作业中,调度器只是一一对应,将对应请求通过共享对象RequestQueue分给对应的电梯的等待队列。

后两次作业,就简单采用计算每部电梯到达请求楼层所需的时间来分配,但是问题比较大,为了各种特殊点修修补补,比如给这个时间加上一定的随机量,对在重置中的电梯不分配,给每部电梯设置请求数上限。至于耗电量之类的参数,感觉花大把时间调参,去争取那一点点的分数很不值,就完全没管。

UML协作图

三次的线程结构没有太大的变化,仅仅是增量开发,连类就只增加了一个简单的锁类。就直接给出第七次作业的UML协作图

img

img

img

img

双轿厢机制

双轿厢的两个轿厢不碰撞的逻辑,我已在第七次作业的分析中提到,用锁来保证最多只有一个电梯能在换乘层,并写定AB电梯不能停留在换乘层,无论是否需要移动,都移动到换乘层的相邻层。

BUG分析

主要的bug还是集中在调度策略的不完善与线程间死锁的问题,之前的每次作业中也提到了。这些同时也是本单元的难点。调度策略尚能通过缝缝补补的方式通过,大不了随机分配摆烂也能得到还不错的性能分,但是线程问题的bug就很折磨了,debug难度很大。

对于debug方法,由于多线程使用常规打断点的方式会出现不同的表现,所以我主要还是通过print方法打印出一部分信息,帮助我发现了很多bug。另一个方法就是在IDEA中,如果程序死锁卡住了,可以点击转储线程,固定住当前线程的栈,分析这些线程都卡在什么位置了,通常就能发现死锁的位置。

心得体会

这一单元的作业难度较大,但我很喜欢这种挑战,从零开始构建一个多线程的系统。这个过程中,我学到了很多多线程的知识,对于多线程的协作、调度有了更深的理解。虽然在开发过程中遇到了很多困难,但是解决问题的过程也让我得到了很大的成就感。特别是在比赛的压力下,压缩开发时间也让我学会了更高效的开发方法,学会了权衡取舍,学会了如何在有限的时间(大约是每次作业4小时内)内完成一个复杂的系统。

...全文
64 回复 打赏 收藏 举报
写回复
用AI写文章
回复
切换为时间正序
请发表友善的回复…
发表回复

301

社区成员

发帖
与我相关
我的任务
社区描述
2023年北航面向对象设计与构造
学习 高校
社区管理员
  • YannaZhang
  • CajZella
  • C_ecelia
加入社区
  • 近7日
  • 近30日
  • 至今
社区公告
暂无公告

试试用AI创作助手写篇文章吧