BUAA OO 第二单元总结

文章详细描述了三次迭代中电梯调度系统的演变,包括UML类图的设计,多线程下的乘客分配策略(LOOK算法和均匀分配),以及对同步机制、reset请求处理和线程安全性的改进。作者通过实际问题和bug修复分享了层次化设计和调试技巧的重要性。
摘要由CSDN通过智能技术生成

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();
        }
    }

这个方法保证如果另一部轿厢正在换乘楼层,则本轿厢会进入等待状态,直到换乘楼层不再被占用。并且在进入换乘楼层时更新floorOccupiedtrue,表明换乘楼层当前被自己占用。

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的架构层次,各个线程各司其职、互相合作,高效地处理请求,并且拥有比较高的拓展性,因此三次作业都能够在这个架构的基础上完成,大大提升了效率。

  • 41
    点赞
  • 44
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值