BUAA-OO-Unit2

OO_UNIT2

一、作业分析及多线程编程

本单元的作业要求我们使用多线程的方式来实现电梯系统,即在同一时间内实现多部电梯对于多个请求的响应。

在多线程的编程实现中我们理想化的结果是多个线程并行运行的过程中能够对于共享对象进行安全的操作,最终实现线程安全的基础上实现更高效率。

由上面的描述我们已经可以看到在多线程编程中我们需要处理的一大要点就是我们需要保证线程安全,这也是我们需要处理的一大问题,为了解决这一问题我们就引入了同步块和锁的概念。

除此之外,在面向多种多样的要求,为了更好的扩展性以及更优秀的代码风格,我们还学习了多种设计模式:单例模式,生产者消费者模型,工厂模式等。

二、同步块以及锁

在本单元中虽然线程安全是一大要解决的问题,但是由于JAVA提供的synchronized锁而简化了许多。在原先需要自行设计如何合理的加锁变成如何合理的设置共享对象。

在第一次作业和第二次作业中,我的共享对象均是只有一个,就是实现的RequestQueue类。这个类是我的储存Request的容器对象,在我的两层生产者消费者模型中的托盘均是由这个类实例化得到的对象。

因此在确定了共享对象后,其实实现过程中那个基本上我的所有的同步锁synchronized基本上只需要加在这个类中就可以,除了为了避免轮询使用wait()的时候。但可惜的是在这几次的作业中我对于同步锁并没有完全理解,因而导致在进入对象内部拿锁之前也拿了锁,结果是每次使用托盘的时候我可能会拿多层锁,虽然这样并没有导致死锁,但是结果是现在会看之前的代码实现会感到十分的草率,总是在盲目加锁。

当然还有同步块处加的锁,这一部分由于前文说过的我加锁加的有点臃肿并没有出现问题,当然现在回看我认为我现在能大概明白在哪些地方需要进行同步块,如:在某一步对于共享对象的操作中是需要依赖于前一步对于共享对象的操作或者对于共享对象的状态判断,这种情况下需要如果不进行同步块的设置就需要提防两个语句之间另外一个线程对共享对象进行操作导致的对象状态变化导致后一步不能正确执行。

在第三次作业中,我也仍然是对于共享对象进行加锁,这次的共享对象有两个MainQueuePersonQueue,其实对应的还是两层生产者消费者模型的两个托盘,并且进入类内可以看到MainQueue中还是一个PersonQueue,至于这样的实现主要是由于第三次作业中需要将需求进行分段之后,每一个电梯完成某一阶段的任务之后需要将下一阶段的PersonRequest重新加入到调度器可以调度的对象中,因此将MainQueue做成单例共享对象。

除了对于共享对象的改变,在第三次作业中由于考虑到自己的实现中对于共享对象的读取需求远远高于写需求,最初想实现读写锁,不过由于自己在实现过程中一直报错并且时间并不是很充分,最终仍然选择了sychronized锁来简单实现。

三、调度器设计以及交互

在三次作业中我的调度器均差不多,由于采用的是自由竞争策略,只需要在第一层生产者消费者的结构中使用调度器将生产者生产的需求分配至对应的楼层。

此调度器由于需要实现根据需求的要求将等待队列中的某一个请求分配到能够处理此请求的楼层中,因此在此调度器中需要得到一个能够访问到每一个Floor Building的容器,因此我设计了两个单例的容器对象FloorMapBuildingMap,这两个容器中的主要属性就是一个HashMap用来访问到每一个楼层来加入请求。

因此调度器与其他楼层的交互都是通过这两个Map进行的。

调度器最终需要实现的功能也并不复杂,最主要的就是 void scheduleRequest(Person person),分配过程中是他作为一个消费者将第一层托盘中的Person根据他的nowRequest来分配至对应楼层中的托盘中。因此它既是消费者也是生产者。主要还是因为实现方向不是将每一个请求根据当前的每一个电梯的状态来直接分配至电梯,所以多了一层缓冲层,因而交互十分简单,也不臃肿。

四、三次作业架构设计

在前几节中说过我的作业的架构宏观上来看就是两层生产者消费者模型,这是在三次作业中均没有改变的,当然这也是我本单元作业中为数不多的设计比较合理且拓展性好的部分。

4.1 两层生产者消费者模型

首先采用两层生产者消费者的目的是在意图实现自由竞争策略的基础上能够将不同的类解耦合并且在对于第三次作业的分段请求中可以更好地拓展。

另外可以看到在两层生产者消费者中有两个部分中会有两个中间对象比较特殊他们在整个框架中既是消费者也是生产者,不过他们所扮演的生产者和消费者是在两个不同的层级中。

4.2 第一次作业架构图

在解释完两层生产者消费者模型其实这一次作业的架构图就没有什么了,就是选择什么样的容器这些细节。

4.3 第二、三次作业架构图

第二三次作业其实架构基本一样,在第一次作业的基础上为了更好地存储和访问楼层信息,新设计了FloorMapBuildingMap,对于第三次作业比第二次作业将PersonRequest封装为Person以便于通过标记nowRequest来处理分段的请求。我在这里主要画出第三次作业的图。

这两次作业架构中,为了实现横向电梯与纵向电梯的状态访问以及增删电梯,运用了两个自定义的容器,然后在第三次作业中为了在输入线程得到一个请求时就能第一时间进行分段,也就是需要访问每一个横向电梯可达信息,增加了一个SwitchInfoMap来作为一个存储可达信息的单例容器。设计成为单例也是因为许多类中都可能会用到这个图。

三、bug分析

3.1 自己几次作业bug分析

在第一次作业中自己的实现出现重大bug,主要原因是电梯的调度策略写烂了,虽然实现的是简单的look策略,但是自己在实现过程中是采用的状态机的思维来完成的,因此我需要对look过程中的每一种情况进行判断来决定下一步的策略,因此需要对各种情况都要考虑到,而程序的bug就出在这个地方,我在对于每种情况判断的过程中由于第一次实现时偷懒了,自以为找到了一个可将电梯内部和电梯外部的等待队列情况统一的判断过程,因此导致了我的电梯在走到look时既没有只接某方向的人,也没有能够在到达最高层或者最底层时掉头,导致我的电梯会带着人冲向天堂或者冲向地狱。因此我在第二三次作业中将队列进行了进一步的细分。

第二次作业中没有发现bug,高并发下也没有出现bug,主要是吸收了第一次作业的教训,在第二次作业中认真测试了自己的横纵向电梯。

第三次作业中出现了一个轮询,(本地测了一天,输出结果一直正确,但是在周日时我被hack的刀数却一直在增加,最后发现时基本上每次别人随便hack另外的人都可以顺手把我刀了个CTLE,可恶)。最后查到是因为横向电梯wait的条件出错的,应该判断他能接的横向请求是不是空之后wait,而我直接判断的是该层的所有横向请求是不是空,导致wait不了,一直在轮询。

回头看,其实每一个bug都是极其愚蠢的,第一个bug体现出了自己在架构和实现时一定不能偷懒,尽量用更合理的架构而不是更好些的架构。第二个bug则说明自己在写代码时还是没有清醒的头脑,将整体思路以及不同分支条件判断正确。

3.2 hack他人的bug

本单元hack他人主要都是(1)策略错误,即类似于我的第一种bug。(2)高并发下的bug,这个是由于部分同学的实现还是线程不安全导致的,这个bug也主要是在第二三次作业中发现的。

四、收获与体会

回头看这一单元,仍然感觉自己对于多线程尤其是线程安全只是理解了皮毛,对于同步块以及锁也只是会最简单的应用,还是需要很多的提高。在本单元中使用了sychronizedReentrantReadWriteLock,其中感觉到对于sychronized的使用较为熟练了,且能较好的理解,但是对于ReentrantReadWriteLock只是在部分小类中使用了,本来希望在整个项目中都是用ReentrantReadWriteLock,但是中间出现了很多不可控的bug以及自己时间不够了最终给放弃了,因此感觉对于锁的使用还是有一点点遗憾。

当然感觉收获颇丰的是对于生产者消费者模式,单例模式等设计模式的理解和使用。让我感受到自己在进行架构时,尤其是第二三次作业中,自己的架构能力有了很大的提升,从第二次到第三次能够感觉到自己轻松了很多(当然这是在没有打算进行性能优化的前提下),仍然感觉有很多提升。

posted @ 2022-05-01 23:31  SKiToul  阅读(29)  评论(0编辑  收藏  举报