BUAA OO Summary - Unit2

第二单元总结

第二单元的作业,由于我在最开始设计架构的时候投入了较多时间,很多地方都考虑了后续作业可能需要拓展的地方,所以我的三次作业整体架构几乎没有什么变动。

整体上采用了生产者与消费者模式,将输入类调度器类电梯类继承Thread,GeneralQueueWaitingQueue则是线程安全类,作为托盘,在其中实现线程之间共享对象的增减操作,保证线程安全,从而在线程类中就不用过多考虑由于操作共享对象可能引起的线程安全问题,使问题“分而治之”,用荣文戈老师的话说就是“铁路警察,各管一段”,一定程度上可以降低问题的复杂度。

第五次作业

 

下面分享一下我整个架构的设计思路。

先分析三个线程类

首先,为了降低程序的耦合度,工程上一般都会把输入作为一个单独的类来处理,原因是为了和内部的核心功能分离开,也可以保证当输入要求改变的时候,内部核心模块可以不受到影响,所以设计一个InputHandler类来单独处理输入是必要的。

第五次作业情况最为简单,在最开始设定了A、B、C、D、E座每座有一个纵向电梯,并保证电梯之后也不会再增加;此外,所有的请求均是在同一座的楼层变动。所以,当从输入线程中获取到乘客请求时,该请求其实已经默认属于某一电梯了(比如请求是从B座2楼到5楼,这一请求就已经注定是要放到属于B座的2号电梯里面来完成的了),而将输入的请求进行甄别并分配到对应的电梯这一工作,为了符合“分而治之”的原则,当然需要单独用一个调度器来完成。所设计了一个Dispatcher类

电梯由于要不断地运行所以理所应当设置为一个线程类,这个就不多论述了。

再分析两个线程安全类

在第五次作业,由于我预感之后可能会出现换乘类的请求,所以在输入类和调度器中间加入了一个GeneralQueue,用于存放从输入类中获取到的所有请求,这样在后面如果有换乘请求的时候,直接装入GeneralQueue视为一般请求即可,有很大的兼容性。

在调度器线程和电梯线程中间,为每一个电梯都设计了一个自己的WaitingQueue,在WaitingQueue中的每一个请求都是在调度器分配下保证一定是由其相应的电梯完成的。这里不直接把这些请求装入对应电梯的原因,主要是为了解决超载的情况并且更好地模拟现实生活中电梯实际运行的情况。我在电梯类中设置了其私有的insideQueue表示已经上了电梯的人,所以这样的话,如果某电梯内部的insideQueue的size没有达到最大容量,即电梯没有超载,就可以直接从WaitingQueue中获取自己的等待请求,如果超载了,就等待电梯中的请求完成后再从WaitingQueue中获取请求。

同步块的设置和锁的选择

这两个线程安全类的方法我都加了synchronized,一开始以为这样是一种很好的设计模式,因为能够很好地保证线程安全,但是知道三次作业完成后上课听到老师说其实这样很不好。最好不要在函数上加synchronized,这样锁的对象是this,这样锁的力度很大,确实很好地保证了线程安全,但是还是推荐使用synchronized(obj),找合适大小的锁去锁,并且synchronized里的代码尽量精简。所有这些都是因为加锁的耗费很大,这样开销很大,性能会受到影响。“杀鸡焉用宰牛刀”,我们所需要做的不仅是保证程序的正确性,还应该尽可能追求高的性能。

调度器设计

调度器主要是通过调用generalQueue的receivePersonRequest方法原子性地获取一个请求,,然后调用其自身的分配调度方法dispatchRequest()将这个请求分配到对应电梯的等待队列waitingQueue中。

这里对于分配到对应电梯的策略基本上遵从基准策略

  • 在第五次作业中,只有纵向电梯,判断请求属于哪一楼座,然后如果该楼座只有一个电梯加入该电梯的等待队列即可,如果该楼座有多部等待电梯,由于他们的优先级一致,所以选择循环遍历给予每个电梯请求,新的请求加入容器中下一个电梯的等待队列中。

  • 第六次作业,加入了横向电梯,由于保证请求要么同楼座要么同楼层,只需要在调度器中加入一个getRequestType()方法判断该请求属于纵向还是横向请求,然后根据其类别再调用buildingDispatch()或floorDispatch()方法即可,这俩方法对请求的调度同第五次作业的描述。

  • 第七次作业,加入了换乘类请求的可能,(即请求的起始楼层和终点楼层与其实楼座和终点楼座同时不同),为了兼容这种情况,在getRequestType()的请求类型中除了纵向、横向类型还有换乘类型,同时dispatchRequest()如果判断是换乘类型就会调用换乘方法将请求拆分为横向、纵向请求再调用相应类别的分配方法。

线程协作

 

如上图所示即为我设计的三个线程之间的协作图,其中绿色箭头是为了满足第七次作业换乘的需求而增加的电梯线程向GeneralQueue装入需要换乘的人的下一个阶段的请求(具体细节在后面第七次作业处说明)。

第六次作业

 

第六次作业相比于第五次作业,主要有两个变动:1、可以通过输入增加电梯 2、电梯种类增加了横向电梯,可以完成同层不同楼座的请求。

对于通过输入增加电梯,只需要修改在第五次作业中的InputHandler类即可,这里充分体现出把输入单独作为一个类的好处

这里由于我设计架构的可扩展性较强,这里只需要把第五次作业的电梯类内部不改变更名为纵向电梯,然后再加入一个横向电梯类,

第七次作业

 

第七次作业相比第六次作业,主要有两个变动:1、新增电梯定制化功能 2、请求的起始楼座、起始楼层和目的楼座、目的楼层可以同时不同,即需要换乘。

同样,由于在第五次作业的时候就考虑到了后面可能会涉及到换乘的问题,所以设计的架构能够很轻松地支持加入换乘。

这里换乘的实现主要依赖于GeneralQueue实现单例模式、增加调度器对换乘类请求的识别并实现将换乘请求拆分、换乘类请求拆分后的某段请求完成后再将下一段请求装入GeneralQueue(单例模式的作用在此体现)

由于整体架构几乎没有改变,给出第七次作业的类图如下:

 

分析bug

自己的bug

第五次作业

  • bug

    • 这一次作业强测没有出错,但是在互测时被hack到,原因是没有保护官方包的输出线程,导致输出被hack,时间戳不能满足递增

  • 修复办法

    • 为输出单独创建一个OutputThread(),通过静态方法加锁实现输出的同步

第六次作业

这一次作业强测互测均没有出现bug

第七次作业

  • bug

    • 这一次作业强测互测都源于一个bug:对于起始楼层和目的楼层一样的请求,直接判为了不需要换乘的横向请求而忽略了可能该楼层的横向电梯不能满足在请求的起始楼座和目的楼座都停靠

  • 修复办法

    • 在判断请求类型的时候,对于起始楼层和目的楼层一样的情况,遍历对应楼层的所有横向电梯,如果该楼层有电梯能在请求的起始楼座和目的楼座都停靠,则判定该请求为横向请求;如果没有,则判断为换乘请求,这样的话就可以到别的楼层换乘,拆分为三段请求(1:在起始楼座,起始楼层->换乘楼层;2:在换乘楼层,起始楼座->目的楼座;3:在目的楼座,换乘楼座层>目的楼层),再调用相应的横向/纵向调度方法即可。庆幸于自己设计的架构很清晰,所以只需要改这里类别判断,别的方法并不受影响,并且直接调用就能满足要求

别人的bug

经过这三次作业,大家的bug主要集中于:

  • 保护官方包的输出线程

  • 没有考虑电梯超载的情况

  • 线程安全没有保证,当同一时间戳输入多个请求的时候出现问题

  • 第七次作业中换乘只考虑了换乘请求拆分为三段请求的情况,没有考虑换乘请求也可能只用拆分为两段

这一单元的测试确实与第一单元都很大不同。第一单元一个输入就会得到一个输出,并且每次的结果都不会改变,容易复现,并且容易debug找到问题出现的地方。而这一单元由于这是多线程协同,很多情况不能复现,debug也比较困难。

我找bug几乎是采用黑盒模式,把输入的极端情况(比如同时多个请求、超载、1楼10楼这种边界位置的转向)考虑到以后测试程序结果是否如逾期,知道这样hack确实不太好,但是确实挺有效的。

心得体会

这一单元其实总体来说感觉完成题目需求比第一单元要简单,所以感觉花费的时间也要少很多。而且第五次作业好的设计架构真的会让后面的迭代开发容易很多,几乎只用根据需求微调一下具体实现细节就可以,让我更加认识到设计的重要性。

但是完成需求,想要真正领悟到多线程协同工作,以及同步块的设置和锁的选择却比较难。比如我在最后三次作业做完后老师上课提到最好不要在函数上加synchronized,这样锁的对象是this,这样锁的力度很大,确实很好地保证了线程安全,开销很大,性能会受到影响。“杀鸡焉用宰牛刀”,我们所需要做的不仅是保证程序的正确性,还应该尽可能追求高的性能。推荐使用synchronized(obj),找合适大小的锁去锁,并且synchronized里的代码尽量精简。这样的话就会需要很明确地知道同步的具体对象是什么。以后我再遇到多线程相关问题的时候我一定会采用这种方法。

posted on 2022-05-01 20:54  流英成和  阅读(32)  评论(2编辑  收藏  举报

导航