BUAA_OO_Unit2总结

Unit 2

第一次作业

题目简述

每一座楼有一个纵向电梯,请求的出发楼座和目的楼座一定相同,出发楼层和目的楼层一定不同。

第一次作业作业类的个数较少,只有5个,其中有3个线程相关的类,分别为主线程、输入线程和电梯线程,1个输出类,1个共享对象类。因为本次作业中每座只有一个纵向电梯,所以并不需要一个调度器线程去分配请求。

代码行数

第一次作业规模也较小,代码行数仅有341行。

思路简述

  1. 采用生产者-消费者模型,Input为生产者,Elevator为消费者,RequestQueues为共享对象。

    • Input线程负责输入请求,并将请求直接放到共享队列中。Elevator线程负责处理请求,并模拟电梯运行。因为本次作业中每座只有一个纵向电梯,所以并不需要一个调度器线程去分配请求。

    • 共享对象的类型为ArrayList<PersonRequest>[],其实就是一个ArrayList的静态数组,静态数组大小为10,分别对应每一座的十层,每一层都有一个ArrayList<PersonRequest>,即请求队列。

  2. 对于纵向电梯调度,我采用了Look策略:

    • 如果电梯中有乘客,将其中到达时间最早的乘客请求作为主请求,并对沿途方向相同的请求进行捎带。

    • 当电梯中没有乘客时,寻找当前电梯运行方向上距离当前楼层最远的请求作为主请求,若不存在,则寻找当电梯运行相反的方向上距离当前楼层最远的请求作为主请求。

线程安全

第一次作业中只有一类共享对象RequestQueues。

  1. 锁的选择

    在锁的选择上,我在三次作业中均使用的ReentrantLock,因为使用一个对象单独作为锁相比于synchronized来说更加方便,但是并没有使用ReentrantReadWriteLock,虽然它在灵活性和性能上都更优,但是使用起来较为复杂,并且对于性能的收益并不明显,所以我并没有采用ReentrantReadWriteLock。

  2. 同步块

    为了使线程中的run方法不那么复杂,我将所有的同步块都封装为了函数保存在共享对象类中,这样做可以使得代码更加简洁且逻辑清晰。在取请求的同步块中加入wait防止线程轮询,并在放请求的同步块中加入notifyall来及时唤醒被等待的线程,同时在setEnd函数中加入notifyall来确保最终所有被等待的线程被唤醒并结束线程。

     public void setEnd(boolean end) {
         lock.lock();
         try {
             isEnd = end;
             condition.signalAll();
        } finally {
             lock.unlock();
        }
     }

UML类图

UML协作图

bug分析

本次作业难度较小,在互测、中测、强测中均没有产生bug,但需要注意一点,官方包给出的TimableOutput类并不是线程安全的,需要用synchronized块加以限制。

第二次作业

题目简述

每一座楼有多个纵向电梯,每一层有多个横向电梯,请求的出发楼座和目的楼座不同或者求的出发楼层和目的楼层不同,但不能同时不同。

第二次作业有6个类,其中有4个线程相关的类,分别为主线程、输入线程、横向电梯线程和纵向电梯线程,1个输出类,1个共享对象类。本次作业虽然在每一座或者每一层有多个电梯,但我采用了自由竞争的方法,所以也并不需要一个调度器,但需要更关注线程安全方面的问题,因为涉及到多个电梯线程同时操作一个共享对象。

代码行数

第二次作业在上一次作业的基础上进行迭代开发,增长了一百多行代码,主要是新增了横向电梯这个新类。

思路简述

  1. 对于横向电梯调度,我采用了一种简单的策略:

    横向电梯按照ABCDE的方向循环运行,到达某一楼座时,送出该目的楼座的所有请求并捎带这一楼座的所有请求。之所以没有采取与纵向电梯类似的Look策略,是因为Look策略应用在环形电梯上时可能会出现死循环的情况,而如果对Look策略进行改进又会引入新的问题,而且算法的复杂度与性能并没有很强的关联,有时候一个简单的策略也会在性能上有着优异的表现,所以我采用了循环的策略,这一策略看似十分简单暴力,但在后两次作业中的性能上表现很好。

  2. 对于请求的调度,我采用了自由竞争的方法,既让同一座或者同一层的多个电梯自行在共享队列中取请求,而不是让调度器给这多个电梯分配请求,这样可以去掉调度器,但是增加了线程安全的问题,因为多个电梯线程会同时操作一个共享对象,我的解决方法是使用一个带有临界块的函数,这一函数可以取出共享队列中的一个请求,如果没有则返回null,每个电梯线程通过调用这一函数来得到请求,以确保多个线程不会同时取出同一个请求。

线程安全

第二次作业中的共享对象类并没有发生改变,与第一次作业完全相同。

UML类图

UML协作图

bug分析

第二次作业在互测、中测、强测中均没有产生bug。

第三次作业

题目描述

每一座楼有多个纵向电梯,每一层有多个横向电梯,请求的出发楼座和目的楼座不同或者求的出发楼层和目的楼层不同,且可以同时不同,即请求需要换乘,同时电梯可以设置速度、容量、可达信息等参数。

第三次作业有9个类,其中有4个线程相关的类,分别为主线程、输入线程、横向电梯线程和纵向电梯线程,1个控制器类,1个输出类,3个共享对象类,CustomRequest为自定义请求,RequestQueues为楼座或者楼层的等待请求队列,RequestCounter为未完成请求的计数器。本次作业采用自由竞争的方式,所以仍然没有设计调度器类,但由于涉及到有些请求需要换乘,所以增加了控制器类管理请求的换乘。

代码行数

第三次作业规模较大,代码行数已经增加到了800行,相比于上次作业多了300行。

思路简述

本次作业采用了类似于exp4_2的流水线架构,新增Controller类来对请求进行分段处理,新增RequestCounter类来计数未完成的请求,以便在所有请求完成后结束所有线程。

  1. Controller类分段请求算法:

    • 若请求的出发楼座和目的楼座相同,则将乘客放到对应的纵向电梯等待队列中。

    • 若不同,判断本层是否有横向电梯可以送达,若可以,则将乘客放到对应的横向电梯等待队列中。

    • 若不可以,判断目的楼层是否有横向电梯可以送达,若可以,则将乘客放到对应的横向电梯等待队列中。

    • 若不可以,判断出发楼层和目的楼层之间是否有横向电梯可以送达,若可以,则将乘客放到等待人数最少的楼层的横向电梯等待队列中。

    • 若不可以,则其他楼层一定有一层可以送达,则将乘客放到等待人数最少的楼层的横向电梯等待队列中。

  2. RequestCounter类:

    • Input接受到乘客请求时,计数器加一。

    • Elevator处理完乘客请求时,即到达目的楼座和目的楼层,计数器减一。

    • 最后判断计数器是否为0,对线程进行结束。

线程安全

第三次作业中新增了一个共享对象类RequestCounter用以计数未完成的请求。

  1. 锁的选择

    在锁的选择上,与第一次作业一样使用的ReentrantLock。

  2. 同步块

    输入线程会在最后检查所有的请求是否已被完成,所以需要在查询计数器时进行wait操作,防止input线程轮询,并对计数器减一时进行notifyall,唤醒输入线程,如果查询到计数器为0,则结束所有进程。

     public void subtract() {
         lock.lock();
         try {
             counter--;
             condition.signalAll();
        } finally {
             lock.unlock();
        }
     }
     public int getCounter() {
         lock.lock();
         try {
             if (counter == 0) {
                 return counter;
            }
             try {
                 condition.await();
            } catch (Exception e) {
                 e.printStackTrace();
            }
             return counter;
        } finally {
             lock.unlock();
        }
     }

UML类图

UML协作图

bug分析

第三次作业因为一个小小的bug导致强测寄了六个点,因为我采用了自由竞争的方法对请求进行调度,所以在判断某一楼层的横向电梯是否可以送达这个请求时,需要判断这一楼层所有的电梯是否有一个可以送达,如果有,那么就将这个请求放到等待队列中,但是当这些横向电梯去同时竞争接这个请求时,我并没有判断这个横向电梯是否可达,这就导致了一些不能处理这一请求的电梯接上了这一请求,这样就出现了bug。

hack策略

因为第三次作业强测寄了六个点,互测在B房,所以我也hack成功了很多次。

  • 由于有些代码电梯的调度策略不太合理,所以可能会出现超时的情况,在hack时可以使用一些数据量较大的测试,并让所有请求在同一楼座同时发出,同时不能在这一楼座增加电梯请求。

  • 因为多线程的不确定性,有些程序存在线程安全的问题,但是这一问题并不一定在一次测试中就会发生,有些可能测试十几次才会复现一次,所以同一数据可以多测试几次。

  • 尽量构造规模大且集中的测试数据,可以hack到一些功能方面的bug。

性能优化

  • 早出晚进,即当电梯开门时,乘客立即出电梯,以便尽早换乘。在开门380ms后,乘客再进电梯,以便捎带更多的乘客。代码如下:

     public void inAndOut() {
         open();
         out();
         try {
             sleep(385);
        } catch (Exception e) {
             e.printStackTrace();
        }
         in();
         close();
     }
  • 量子电梯,记录电梯上次关门或者移动的时间,如果当前时间与上次时间相隔很久,则可以直接移动到下一层,代码如下:

     public void move() {
         curTime = System.currentTimeMillis();
         try {
             sleep(speed + lastTime - curTime > 0 ? speed + lastTime - curTime : 0);
        } catch (Exception e) {
             e.printStackTrace();
        }
         curBuilding = (char) ((curBuilding - 'A' + direction + 5) % 5 + 'A');
         Output.println("ARRIVE-" + curBuilding + "-" + curFloor + "-" + id);
         lastTime = System.currentTimeMillis();
     }

心得体会

  • 重设计轻实现

    我们应把大量的时间花费在设计上,而不是急于实现。当我们设计完成后,我们就应该知道我们的代码可以完成哪些功能,而又不可以完成哪些功能。实现只是对设计的代码化,个人认为这是一个比较机械的过程。如果并没有完成设计就开始了代码实现,此时思路并不清晰,极易出现bug,而且当这一设计走不通时,代码还要重写。

  • 及时重构

    当我们需要对工程的功能进行扩展时,如果我们当前的架构已经不能实现该功能,或者我们为了实现这一功能导致架构很不优雅,这时我们就需要及时对我们的架构进行重构,设计出一个更好的架构。

  • 单一职责

    每一个类都应该有自己所应该履行的职责,并且这个职责是单一的。比如共享对象类,应该确保自己的线程安全性,确保任何线程调用自己的方法都不会出现线程安全的问题,而不需要线程本身去维护,线程类则只需要专注于自己的业务。如果不注重单一职责,则各个类之间互相依赖,关系错综复杂,极易出现bug。

  • 全面考虑

    多线程程序比较复杂,我们需要考虑到所有可能会发生的情况,并对一些操作进行原子化,防止因为线程不安全而产生bug,并且这类bug一般不易复现。

posted @ 2022-05-04 15:15  隐姓埋名4567  阅读(30)  评论(0编辑  收藏  举报