OO第二单元博客

OO第二单元博客总结

作业总述

第二单元电梯作业是对于面向对象中多线程内容的考察。在本单元的作业中,由于开始阶段对于多线程的不熟悉,我参考借鉴了上机课的架构,从第一次作业开始便采用输入——调度器——电梯处理的模式,并一直将这样一种架构进行功能的扩展,完成了本单元的作业。除具体方法不同外,三次作业的类没有进行删改,如下:

  • MainClass:在主类中完成输入、调度器和多个电梯线程的启动工作。

  • InputThread:输入线程,在此线程内可以完成乘客的请求、加电梯请求的输入,并将其添加到总的等候队列中。

  • WaitQueue:总的等候队列,定义了其中的方法完成对候乘乘客的管理。

  • Scheduler:调度器,将不同的请求按规则分配给不同的电梯队列。

  • ElevatorQueue:每个电梯的乘客队列,定义了一些方法对乘客的状态进行判断。

  • Elevator:电梯线程,负责根据请求进行上下移动和运输乘客操作。

在第一次作业中,要求实现单个可稍带电梯,为了之后作业的扩展性,我选择对单电梯仍然采用总等候队列--调度器--电梯队列--电梯的处理方式,只不过此时的可分配电梯仅改为一部,作为第二次作业的特例。第一次作业在调度算法上为了保证与指导书保持一致(害怕在某些极端数据下自己写的其他算法会因超时挂掉强测点),采用了ALS调度算法,对于晚上则采用向上找到6个人便回来的策略,对于早晨采取延长一楼等待时间的策略,然而在强测中发现,ALS的性能实在可怜,导致很多测试点统一85分,实在扎心。

第二次作业,由于上次作业性能分的惨痛教训,早早便修改了电梯的调度策略,改为Look算法,若电梯上行,则将当前电梯内所有上行乘客运送完毕,先寻找是否有更高层的向上请求,若无,再寻找最高层的需要下行的乘客请求,将需要下行的乘客一起带下去;同理,电梯下行时,将所有需要下行的乘客运送完毕后,先寻找是否有更低层的下行请求,若无,寻找最低层的上行请求,将需要上行的乘客一起带上去。由此,单电梯的性能得到大幅提高。此外,还将各种模式采取了同样的处理方式,理由是对于晚上,这种算法下的电梯会运行到最上层再依次接人下行,效率很高,对于早上,由于多电梯的存在,也不会造成很大的性能损失。对于多电梯的调度,我通过比较各个电梯的等候队列长度,将新请求分配给当前请求最少的电梯,以求得每个电梯的送客人数尽可能均衡。

第三次作业,也是在之前的架构上对于电梯的属性和调度器进行了部分改进,没有进行大改,顺利完成了作业要求。

同步块的设置和锁的选择

由于我的三次作业架构都比较固定,所以从第一次作业开始,同步块和锁的设置便一致进行了沿用,后续作业中,只有少量对于加锁部分的补充,所以可以一起来总结概括。

首先是输入线程和调度器线程都会对于waitqueue进行读写,在input线程中,将新加入的乘客请求写入waitqueue中,在读取到null时,也要将waitqueue的状态置为end。而调度器进程则要将waitqueue中的元素读取出来,并将其分别加入到各个电梯队列之中,在waitqueue中将其删除,所以在对waitqueue进行操作时,都使用synchronized进行了加锁。当调度器线程中发现waitqueue为空时,进入wait状态,等input线程输入新的请求后才进行notify。

InputThread:

if (request == null) {
                   synchronized (waitQueue) {
                       waitQueue.close();
                       waitQueue.notifyAll();
                  }
                   break;
              } else {
                   if (request instanceof PersonRequest) {
                       PersonRequest personRequest = (PersonRequest) request;
                       synchronized (waitQueue) {
                           waitQueue.addRequest(personRequest);
                           waitQueue.notifyAll();
                      }

Scheduler:

synchronized (waitQueue) {
               if (waitQueue.isEnd() && waitQueue.noWaiting()) {
                   for (ElevatorQueue elevatorQueue : elevatorQueues) {
                       synchronized (elevatorQueue) {
                           elevatorQueue.notifyAll();
                      }
                  }
                   return;
              }
               if (waitQueue.noWaiting()) {
                   try {
                       waitQueue.wait();
                  } catch (InterruptedException e) {
                       e.printStackTrace();
                  }

此外,输入线程和调度器线程还都对电梯队列的队列(即elevatorqueues)进行了修改,input线程中若读取到加电梯的指令,则将原电梯队列中的处于停止状态的一个队列进入运行状态,而调度器则要对每个电梯队列中的总乘客等进行读取,自然要对电梯队列的队列进行读写操作,所以在涉及elevatorququeues的代码块,也用synchronized进行了加锁,在顺序执行完之后,两个线程都会自动放锁,让其他线程继续执行。

之后便是调度器与电梯进程对于电梯队列的数据共享。调度器负责从等候队列向电梯队列中加入乘客,电梯则到达相应楼层后对电梯队列中的人进行扫描,并接送乘客,将其从队列中清除。所以对于elevatorqueue进行操作时,也通过synchronized进行了加锁,调度器线程每次向队列加入乘客后,会进行notify,而电梯则会在乘客列表为空时进入wait状态,收到notify后继续运行。

Scheduler:

synchronized (elevatorQueues) {
                           for (ElevatorQueue elevatorQueue : elevatorQueues) {
                               synchronized (elevatorQueue) {
                                   if (!elevatorQueue.isRun() ||
                                           !elevatorQueue.getType().equals(type)) {
                                       continue;
                                  }
                                   if (elevatorQueue.size() < least) {
                                       least = elevatorQueue.size();
                                       bestQueue = elevatorQueue;
                                  }
                              }
                          }
                           synchronized (bestQueue) {
                               bestQueue.addRequest(request);
                               bestQueue.notifyAll();
                          }
                      }

Elevator:

synchronized (elevatorQueue) {
           for (int i = 0; i < elevatorQueue.size(); i++) {
               PersonRequest request = elevatorQueue.getResqut(i);
               if (request.getFromFloor() == currentFloor &&
                       passengers.size() < elevatorQueue.getRoom() &&
                      (getState(request) == state || state == 0)) {
                   TimableOutput.println(
                           String.format("IN-" + request.getPersonId() + "-" +
                                   currentFloor + "-" + elevatorQueue.getId()));
                   passengers.add(request);
                   elevatorQueue.remove(i);
                   i--;
              }
          }
      }

调度器设计

在第一次作业中,调度器的作用其实挺小的,如果不考虑后续的作业,这个调度器仅仅是将总的等候队列中的乘客分配给了单一电梯的等候队列,在第一次作业中加入调度器是将其作为多电梯的特例来对待的,并不需要太多的介绍。

第二次作业中,没有想到更高级的分配算法,最终决定采用分配给等候队列最短的电梯的方式。该方式与均分还有些不同,是关注在请求到来的时刻哪一电梯剩余的请求量最少,从而使多部电梯的运行时间更加均匀,且通过电梯的LOOK算法,效率也能够得到一定的保证。调度器要做的便是对所有电梯的等候队列进行扫描和比较,从中选出待运送乘客最少的电梯,将请求分配给此电梯。

第三次作业中,为了保证程序的正确性,经过权衡之后并没有采取换乘的方式,而是以电梯的运行速度进行排序,C电梯的运行速度最快,则能够用C电梯运输的高低层请求将优先给C电梯,然后除此之外的奇数层请求由B电梯运输,最后剩余的请求由A电梯运输。在同类电梯中,仍然使用第二次作业中的做法,将请求分配给当前等候队列最短的电梯。

优点

  • 将请求队列分配给当前请求队列最短的电梯,能够使电梯的运行时间较为均匀。

  • 分级分配给各类型电梯,使特别的请求能够得到最适合的解决方式。

  • 无换乘的策略下,没有对请求进行更多的操作,程序的正确性更高。

缺点

  • 会出现将相似的请求分配给不同电梯的情况,导致多个电梯同时前往相近的楼层。

  • 无换乘的策略无法应对极端数据,容易使某一类型的电梯一直运行,而另外类型的电梯长期停止的问题。

第三次作业架构设计

UML类图

UML协作图

可扩展性分析

我认为我的架构的可扩展性是较为良好的,但其中仍然存在很多问题。首先,从第一次作业到第三次作业,我并未进行架构的重构,都是在原有的基础上进行了修改和补充,甚至可以将第一次作业的类沿用到最后,且各个类之间的关系与层次也是比较清晰的,这其实也是对我的程序扩展性的一个验证。

对于多电梯的分配,我的程序基本可以将输入--调度--运行进行很好的分工,分别由输入线程、调度器线程、电梯线程进行处理,若改变分配策略,只需要对调度器的逻辑进行修改,无需进行很大的改动,实际上在第二次作业到第三次作业的过程中我也是这么做的。若存在不同电梯类型,只需将对应的电梯属性进行修改,由输入线程以电梯类型改变电梯的属性。

对于单电梯的调度,我的电梯策略的不同其实只包含在电梯类的findmain方法中,策略即寻找“主请求”的方式,之后电梯的其他运行方法完全不受影响,所以对策略的修改仅需要修改此方法即可,不会对程序造成很大的影响,实际上在我从ALS算法改为LOOK算法的时候,也只是改变了电梯寻找主请求的方式,不需要对其他部分进行修改。

但是从上述UML图中也能看出,电梯类中包含了太多的方法,有点属于老师所说的“上帝类”,而其他类职能就比较专一。如果要更好地实现单一职责原则,有必要将电梯类中的策略相关内容转移到另一个类中,从而更好的实现SOLID原则。

自己程序的Bug

三次作业在强测和互测中并未出现导致错误的bug,但出现了影响效率的bug(虽然不影响正确性,我还是愿意称其为bug)。

第一次作业的课下是采用ALS算法进行调度的,由于实现方法与标准ALS算法可能有出入,加上自己morning模式的处理比较拉跨,导致每次电梯在一楼停两秒钟,然后再以ALS的方式运送乘客,直接导致几乎所有morning测试点的性能分为0,且本地测试很多都超出了ALS的1.05倍时间,修改的方式很简单,直接改掉调度算法,从根上杜绝超时。在修改LOOK算法时,出现了诸多细节上的bug,比如调转方向,未考虑本层请求等,导致在本地测试时出现了电梯反复横跳就是不上客人等迷惑行为,不过在课下自测阶段便完成了对于bug的修复,并未对后续作业造成影响。

第二次作业强测中也出现了影响效率的bug,而且确实是bug,与同步性有关。现象是出现了在起始位置添加的电梯被吃掉的情况,这一现象在night模式下尤其显著,因为此时所有的请求同时到来,所以我的程序在强测很多测试点中用3个电梯运完了本来四五个电梯的工作,神奇的是,强测中两个180s才开始输入数据的点居然没有超时,206s极限运输完成。最后发现,是在MainClass中先启动了input线程,再由电梯队列启动电梯线程,而此时的电梯队列没有加锁,使得input中将电梯状态由停止改为运行的操作没有被读取,电梯一直静止,从而导致性能掉点。

第三次作业由于我的改动较小,只是改变了调度器的分配策略以及增加了电梯的速度与容量属性,在前两次的bug修复后,第三次作业没有发现bug。

发现别人程序bug所采用的策略

由于没有实现测评机,只有简单的数据生成方法,而一般性的数据又很难找到同房间内其他人的bug,所以只好采用读代码的方式来进行发现bug了。

与其说是发现bug,不如说更多的是一种学习,因为我接受多线程确实比较慢,读其他人的程序是为了提高自己对于线程安全的认识。分析其他人的加锁解锁和wait,notify过程,同时在策略上也加以学习。比如第一次作业我最拉跨的morning模式,同组内其他人有人将其与random一样对待,有人在一楼每上一个人等待两秒,直到满员再向上运行,不过无论如何,都比我的效率更高。又如第三次作业中参考其他人的换乘策略,有人将中间某段的路程拆给B类电梯,减轻A电梯面对极端数据时的负担。

但是在bug方面,一方面由于同房间内的人确实bug太少了,而且线程安全在我看来都是很优秀的,是值得我学习的。仅发现同组成员存在一种bug,即第三次作业中由于策略问题对于极端数据的超时问题,但是这种极端数据我自己的程序即使不会超时,也没法在短时间内跑完,无法以此来hack别人。

对于未使用测评机测试的人,其实互测耗费精力还是很大的,我感觉对我来讲,更多的是对于大佬们的代码进行学习。也可能房间分配使得我所在的房屋比较安静,大家都挺难找到bug的。本单元的hack难度明显比第一单元大很多,首先是程序运行时间的差异,第二单元的测试一组测试点需要很长的时间,而第一单元输入样例可以立刻得到结果。更重要的是由于多线程运行顺序的不确定性,导致在代码存在某些不稳定因素的情况下,得到的结果是不同的,而且有些错误是很难复现的,这就与第一单元确定的输出具有很大的差异。

心得体会

本单元多线程的学习确实是我之前没有接触过的,在前期也感受到了接受多线程的吃力。于是我从课下训练开始,由最基础的部分逐渐加深自己的认识。

对于多线程的线程协作与交互是在第一次上机时加深的印象,随之而来的便是第一次作业自己的实践,在我自己的课下实践中,我没有出现死锁问题,但却出现了一直wait无法结束进程,轮询导致CPU占用时间过长等诸多问题,在解决这些问题的过程中,我对于多线程的交互理解更加深刻。

对于多线程的线程安全其实我一直在注意,我在写代码的过程中一直注意加锁的顺序问题,不让程序出现各自占用一个资源而不放锁的情况,也取得了很好的效果。不过还是在程序中出现了线程安全问题,是由于对于主类放松了警惕,导致在主类和输入线程中出现数据读写不一致,电梯无法运行的问题。所以也体会到,线程安全确实是多线程中重要且需要警惕的问题。

对于层次化设计,个人的总结是,较第一单元有提升,但回头看自己第二单元的设计,其实也不是非常完善。比如在电梯类这样一个控制电梯移动的类中加入了方法来判断乘客的某些状态,显然相关的方法应该在等候队列中编写,这是编写程序时为了方便而导致的层次化的败笔。不过整体来讲,在线程交互的过程中,还是能感受到层次较之前的程序来讲更加分明了,不过还是有很大的进步空间。

 

posted @ 2021-04-26 00:11  19231223王翔宇  阅读(61)  评论(0)    收藏  举报