BUAA OO 第二单元博客

前言

这一单元中我遇到的困难比起上一单元有过之而无不及,由于我之前根本没有接触过多线程,对多线程的理解十分浅显,并且对 java 中有关多线程的很多东西也都是现学现用,导致我这三次作业都做得十分痛苦,但是这一单元对我的收获也是不言而喻,现在我对多线程的理解可以说是更上一层楼,对多线程的编程方法以及 debug 的方法基本上都有所了解并付诸了实践。

第一次作业

1. 类图

在这里插入图片描述

2. 代码架构分析

首先是整体架构的解析:

由 MainClass 实现对总表、电梯线程、输入线程的初始化,然后由输入线程处理输入并将请求放入总表中,每一个电梯线程都是直接从总表中获取请求,并对请求进行处理

在第一次作业中我并没有设计调度器,采用的是自由竞争的调度策略,并采用了生产者消费者模式,其中输入线程作为生产者,电梯线程作为消费者,总表充当盘子的载体,且盘子的容量为无限

对于单部电梯,我采用的策略如下:

  1. 如果当前没有主请求,即主请求为 null,那么就从总表中获取一个请求作为主请求
  2. 电梯去 pick 主请求,在 pick 到主请求后,将电梯运动方向设置为主请求方向,while 主请求不为空,电梯持续运动
  3. 每到达一层就判断是否有人要进出电梯,如果可以顺路捎带并且电梯容量足够就从总表中直接取出该请求,然后开关门
  4. 更新主请求为当前电梯内部人员队列的第一个人,若电梯内没有人,则退出循环,回到步骤 1

该策略的效率其实很低,后面第三次作业时我对单部电梯的运行策略进行了重构

3. 同步块的设置和锁的选择

第一次作业中线程之间的关系实际非常简单,消费者和生产者都只对总表进行读写操作,所以只需要对总表中的每个方法加上 synchronized (等同于锁 this,即当前这个类对应的对象)即可保证线程安全

4. 复杂度分析

在这里插入图片描述
在这里插入图片描述

第二次作业

1. 类图

在这里插入图片描述

2. 迭代设计

第二次作业延续了第一次作业的架构设计,仍然采用了自由竞争的调度策略,并且电梯本身的运行策略也没有进行改动

主要进行迭代的部分其实是输入线程增加对 add 和 maintain 请求的处理,总表增加 toBeMaintained 符号位,电梯中主要的改动在于线程的退出条件:

  1. 电梯内没有人、总表为空、输入结束、总表中 toBeMaintained 符号位为 false(即没有电梯在 maintaining)
  2. 或者电梯本身要被 maintain 且已经完成了 maintain 操作,即 isMaintain 为 true 且 maintained 为 true

在这一次作业中,电梯本身作为消费者的同时,也可能摇身一变成为生产者,这个问题看似简单,实际上并非如此,因为要考虑到各种 maintain 指令发出的时机,对应的电梯处于哪一种状态时要做出哪一种处理,如果少考虑了任何一种情况,就可能导致程序的错误运行,而且对于电梯线程的结束条件要十分谨慎地处理

3. 复杂度分析

在这里插入图片描述
在这里插入图片描述

4. bug 分析

在这一次的强测中,我不幸挂掉了 10 个点,这 10 个点都是因为同一个 bug 产生的 :
如果电梯在运完乘客退出循环,正好在将要判断自己是否要结束线程时,出现了一个 maintain 指令,线程将直接退出,而不能正确地输出 miantain-able 信息

在之前的 debug 过程中我完全没有考虑到这种 bug,由此可以看出多线程 bug 的隐蔽性(多线程的设计确实烧脑)

第三次作业

1. 类图

在这里插入图片描述

2. 重构架构

在这一次作业中,由于需求的变化,我对整体的架构进行了很大的改动

以下是对本次架构的解析:

首先还是 MainClass 实现对 Elevator、ElevatorQueue、RequestDispatcher、ElevatorCenterController 以及 InputHandler 的初始化,其中的线程是 Elevator、RequestDispatcher、InputHandler,在 InputHandler 中去读取各种请求,人员运输请求会被放入总表,RequestDispatcher 负责将总表中的请求分配到分表中,每个电梯都有其对应的独立的分表,从独立分表中获取请求并进行相应的处理,由 ElevatorCenterController 负责去限制每一层开门的电梯数量,并由 ElevatorQueue 实现对电梯队列和电梯分表的维护

在本次的作业中,有多层生产者消费者关系,其中输入线程是总表的生产者,RequestDispatcher 是总表的消费者,同时是分表的生产者,Elevator 是分表的消费者,又是总表的生产者。除此之外,我又将 ElevatorCenterController 设计为生产者消费者模式,电梯的开门行为是该层的生产者,关门行为是该层的消费者,在ElevatorCenterController 中使用了两种类型的盘子,两种类型盘子的数量不同,一个为 4,一个为 2,分别表示该层服务中电梯数量和只接人电梯数量,并使用链表存储每一层两种盘子的数量

由于感到电梯运行效率不佳,我又对电梯本身的运行策略进行了重构:

  1. 若电梯内没有人,将电梯运动方向设置为去 pick 分表中第一个人的方向,然后电梯开始运行
  2. 每到一层就看是否可以进出,进电梯的条件为与电梯运动方向相同且起始楼层等于电梯当前楼层,若发生了进出,就重新设置电梯运动方向为电梯内部人员队列中第一个人的运动方向,若电梯中没有人则返回步骤 1

这一次电梯的单独运行策略表现优于第一次的设计,在大部分情况下实现了总体运行时间的缩短

3. 调度器分析

本次作业的调度方法设计逻辑如下:

  1. 首先判断有无可直达电梯
  2. 若有则直接调用 dispatch 方法
  3. 若无则先调用 search ,找到换乘电梯数量最少的所有路径中乘客要换乘的第一部电梯以及乘客如果乘坐该电梯所要到达的楼层数,用 ArrayList 存储所有满足条件的电梯,用 HashMap 存储电梯的 id 和要到达的楼层,然后在调用 dispatch 方法
  4. 在 dispatch 方法中我采用的代价衡量的方式进行分配,即对所有满足条件的电梯进行代价评估,找出代价最小的电梯后,把人 push 到该电梯对应的分表中

有关 dispatch 方法中的代价计算方式:

  1. 若该人的起始楼层在电梯的运动方向前方,则代价 a 为楼层差的绝对值,否则为楼层差绝对值的 3 倍
  2. 若该人的运动方向同电梯的运动方,则代价 b 为该人起始楼层和终点楼层的绝对值,否则为绝对值的 3 倍
  3. 若电梯等待队列中等待人数与电梯内部人数之和超出了电梯容量的 1.5 倍,则代价 c 为超出的人数乘以 15
  4. 对于该部电梯来说,最终代价为 a + b + c

该调度方法一方面在一定程度上保证了电梯大部分情况下可以做到捎带乘客,另一方面又避免了在某些极端的情况下,总是由单部电梯接客导致总体运行时间过长

4. bug 分析

本次作业的 bug 是因为调度器在特殊情况下运行结束的判断条件没有写对,在强测中挂掉 3 个点,并在互测中被 hack 到了一次

如果此时输入已经结束,但是有电梯正在被 maintain (即电梯状态为 isMaintain && !maintained)或者电梯正在运载换乘乘客 (即遍历所有分表和电梯内乘客,存在乘客 isToTransfer),则调度器线程不应该结束,而是应该被挂起

5. 复杂度分析

在这里插入图片描述
在这里插入图片描述

三次作业中易变与稳定的内容

在这三次作业中我经历了第一次大规模的重构,但是即使大规模重构,我发现仍有一部分的代码是具有很强的稳定性的,这一部分代码就包括了输入线程和电梯的运行代码:

  1. 有关于输入部分,由于采用的是官方的输入包,且输入线程功能十分单一,在重构过程中,该部分几乎没有什么逻辑上的改变
  2. 有关电梯运行部分,虽然电梯的运行策略多种多样,但是将电梯的行为拆解到最小单元后,这些表示电梯某一个特定行为的方法几乎没有任何改变,究其原因也是功能的单一性和必要性

有关易变的部分,我认为主要是策略的选择和调度器的设计:

  1. 在这三次作业中,电梯自身的运行逻辑都是在不断更改的,一方面是由于需求的改变,另一方面是由于对电梯运行效率的追求,但这一部分其实是和电梯运行的原子操作相分离的
  2. 此外,我认为总表向分表分配请求的的调度方法也是易变的内容之一,虽然我前两次都只采用了自由竞争策略,但是在第三次的作业中我其实尝试过不下三种的调度策略,但有几种效果实在不是很理想,我单独将调度策略抽离出来就是为了方便尝试不同的调度方法

为了适配迭代需求,我认为易变的部分要实现和其他部分的充分解耦,否则会导致牵一发而动全身,如果代码规模不断增大,很可能导致添加新功能会变成一件十分困难的事

DEBUG 方法

由于多线程的并发性,debug是一项非常艰巨的任务,我所能想到的方法只有在适当的地方增加输出语句。例如,如果程序没有正常结束,或者结束不了,就可以在每个线程退出语句前输出线程的名称,这样就可以知道具体是谁没有正常退出;又或者有乘客没有到达目的地,那么可以全局追踪该乘客,尤其应该在调度器中输出乘客的分配信息,比如乘客 id 和乘客被分配到了那一部电梯,再去跟着这一部电梯的具体行为等等;有关 ctle 的bug,可以在每个线程的 run 方法里面增加输出语句,如果输出被该语句持续刷屏,那么就可以说明该线程没有正常 wait 或者被异常 notify。总而言之,多线程的 debug 需要一定的经验,多多练习才会熟能生巧。

总结

这一单元的任务给了我很大的收获,一方面,我学会了很多有关多线程的知识,并且进行了实践练习,对多线程的理解有了很大的进步,另一方面,重构的经历让我明白了初期架构的重要性,第一次作业的架构往往可以决定到后续作业的任务量,或者在不可避免要重构的时候,应该尽快进行,并进行充分地测试,相信在一次一次的重构中,我对层次化设计的认识会更加深入。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值