2022 OO第二单元

2022 OO 第二单元总结

一、同步块的设置和锁的选择

  首先是输出线程的安全性。输出不应该单独建立线程,否则容易导致时间戳不递增的现象,不符合输出要求。如下代码所示,应对官方包所提供的输出进行同步封装。

public static synchronized long println(String str) {
    return TimableOutput.println(str);
}

  其次,是同步块的设置。由于在本单元三次作业的架构设计中,共享的写入数据仅有读入请求的等待队列以及候乘表两项,因此需要对这两类数据的读写进行同步处理。

  在处理所有请求的等待队列时,由于仅涉及将新的乘客请求插入队列和将队首的请求取出加入候乘表两类操作,故采用阻塞队列 BlockingQueue ,其支持阻塞读写操作并会在无内容时阻塞等待。 对其进行 synchronized 处理的目的是在队列中加入请求、从队列中取出请求、标记全部请求读入完毕等操作后及时唤醒对应的线程。

  在处理候乘表时,需要判断线程终止的条件。为避免轮询,在当前候乘表为空时设置了 wait 方法并进行 synchronized 处理,避免 CPU 的频繁查询。

  由于设计中需要上锁的对象较为单一,且读入线程在读入所有请求完毕后不再与调度器等线程进行交互,因此没有必要设置锁,同时也避免了死锁现象的发生。

二、调度器设计与线程交互

  由于本单元作业中电梯的调度策略为 LOOK 策略,多个电梯对于同一请求自由竞争,调度器的主要功能为判断读入线程的终止,及时唤醒处于等待过程中的电梯线程,并将请求加入到候乘表中。同时,在第三次作业中,调度器可以判断可行的较优中转楼层,对换乘请求做初步处理。

  总体来讲,调度器线程与候乘表类和读入线程共享对象;由于候乘表类与所有电梯共享请求并记录特定电梯的信息,因此调度器通过候乘表类间接与所有电梯线程交互,能够实现共享、同步等多重需求。

三、线程协同与架构设计

   主要采用了生产者-消费者模式进行设计,本设计中对于实验三例程参考力度比较大。

3.1 第一次作业 & 第二次作业

  如上图所示,为第一次作业和第二次作业的 UML 图。第二次作业相对于第一次作业添加了环形电梯,并将每个电梯的请求队列更改为相同类型的电梯共享同一请求队列,故第一次作业向第二次作业迭代的力度不大。

  由于在本单元作业的设计中只大多数情况下仅需要为候乘表上锁,因此第二次作业中存在一处非常 bug 的设计。由于没有换乘请求,故只有一个 WaitTable 类,横向电梯和纵向电梯两类电梯各对其进行一次实例化;为了兼容两类电梯,采用了 10x10 的请求队列堆叠方式。其优势是实现简单,在进行候乘表写入时,上锁互不干扰,在性能上有那么一点点的优势。其劣势是显然扩展性较差,多开出了不需要使用的内存,且不能支持第三次作业中的换乘需求,以至于在第三次作业中需要先重构

3.2 第三次作业

  第三次作业的 UML 图如上所示。由于在重构过程中保持了一定的对于第二次作业中 WaitTable 类的依赖性,将新的候乘表类命名为 Map . 相对于前者,采用 ArrayList 容器存放楼层 Floor ,在每个楼层中分别存放该层所拥有的横向电梯、横向电梯请求以及纵向电梯请求;此外,采用 HashSet 容器为横向电梯配置楼层的停靠信息。

  对于换乘请求,采用了静态选取换乘楼层、动态分配新增请求的方式。 静态选取换乘楼层中,由于横向电梯的运动范围为 5 个楼座,而纵向电梯的运动范围为 10 个楼层,平均来讲等待纵向电梯的时间较长,出于对等待时间需求的考虑,应尽可能将换乘楼层安排在换乘请求的起始楼层或目标楼层;如果两者均不能满足需求,再考虑两者之间的楼层是否有能满足要求的楼层,最后考虑其余楼层。因为不清楚是否存在初次计算时横向电梯还未配置好的情况,如果换乘楼层选取失败,会将该请求放回等待队列中。新增继承自 PersonRequest TransRequest 类,根据确定好的换乘楼层伪造请求,在候乘表中添加请求、在电梯开门接入请求等操作中均需要通过 instance of 方式进行特判,在送出请求后判断根据所在位置信息和目标位置信息设置请求完成标志,或伪造新的请求。

  对于线程终止条件,使用计数器的方式,判断是否所有换乘请求均被满足,但是出现了轮询的 bug,因此思考了另外一种设计方式,如下。

3.3 关于架构设计的进一步思考及 bug 分析

  由于第三次作业的情况不太理想,认为可能是架构设计上存在问题。为了沿用第二次作业的大多数方法,减小迭代力度,更应该采取如下所示的策略。

   将非换乘请求从官方包继承为 NormalRequest 类,其和原有的 TransRequest 类并列,两者实现类似的伪造请求方法,可以避免使用 instance of 方式特判换乘请求(主要原因还是太菜,换乘请求在很多位置都没有及时添加,导致debug时间长于写bug).

  由于实现了电梯抽象类 AbstractElevator,有两种类型的电梯对其进行继承,但没有充分发挥横向电梯和纵向电梯的对偶特性,充分发挥层次化设计的优势。具体来讲,对偶性表现在,横向电梯的 position 对应了纵向电梯的 buildingId,而纵向电梯的 position 对应了横向电梯的 floorId,这导致了 debug 时不断地复制粘贴,对照修改,增加了时间成本,同时要注意对于横向电梯楼座的配置进行特判。

  最后是和线程终止问题相关的轮询 bug. 如果只是沿用了第二次作业就不会发生轮询了,那问题显然是在于换乘请求没有被满足但未被重新伪造动态添加到候乘表时 CPU 的频繁查询,相当于第一部分所述的 wait 方法白添加了。改进的方法可以包括但不限于将两者进行配合处理。此外,可以在调度器中静态添加一个换乘请求下的所有子假请求,在所有请求中设置一个 boolean 变量标志该请求是否被激活,每完成一步就根据乘客的 ID 信息将下一步激活。在某一子请求结束但请求未完成的情况下,使用 notify 方法唤醒等待中的线程。

  对于轮询的出现,主要原因还是在于太菜对于线程交互的关系理解不够深刻时间不够了。在本单元作业中出现的其它 bug,比如输出线程没封装、封装了但不同步、同步了但是没改调用等等都是非常沉重的教训,这也成为了 hack 别人的好方法(不是),只要在同一时刻多投喂一些请求就可以了。另外,电梯的运行逻辑中关于停靠楼座配置以及转向的逻辑比较混乱,出现这种问题的原因主要在于为了让电梯在没有请求的时候停下等性能方面的原因牺牲了代码的可读性,导致了维护和重构的困难。

四、心得体会

4.1 线程安全

  我理解的线程安全问题主要在于如何合理地给共享读写数据上锁,包括通过设置该方法的静态特性以决定其是否同步等。此外,合理地设置 wait-notify 逻辑可以避免死锁、轮询、线程不终止等问题,还需具体问题具体分析。

4.2 层次化设计

  总体来讲和算法及实现无关的层次化设计问题不大,但如果涉及到具体问题那就是设计得还不够合理,如上所述。本单元作业加深了我对于引用类型传递的理解,虽然我认为具体实现的过程中不存在该方面的冗余代码问题,但是存在自由竞争架构下的通病,也即所采用的设计模式导致对象大量共享,宏观上引用关系传递复杂等问题。

posted @ 2022-04-29 18:33  RacerK  阅读(29)  评论(0编辑  收藏  举报