BUAA OO 2021 Unit 2 总结

BUAA OO 2021 Unit 2 总结

同步块设计

Homework 5

在第一次作业中,我的同步块设计大多用在了制造线程安全类 WaitingQueue 上,这个类维护了每一层所等待的乘客请求列表,其他线程可以向该类的实例中插入一些请求或是取出一些请求,或是查询所有乘客请求的数量等。该类有一个 HashMap<Integer, BlockingQueue<Person>> 私有成员,由于 BlockingQueue 是 Java 的自带线程安全类,而 HashMap 不是,因此我的线程锁是加在该私有成员上的,所有对内部 BlockingQueue 的操作都需要首先获得该 HashMap 的线程锁,保证了类的线程安全。

同时,为了避免轮询,每个电梯作为一个独立的线程,都会在请求队列为空时进入等待状态。在我的设计中,电梯获取请求并非是竞争式,而是由调度器将请求添加至电梯后,向被加入的电梯发出 notifyAll() 信号离开等待状态,因此还需要在调度器中编写唤醒相关的代码,获取锁后唤醒等待状态的电梯。

if (elevator.getState() == State.WAITING) {
    synchronized (elevator) {
        elevator.notifyAll();
    }
}

Homework 6 & Homework 7

由于在上一次作业中已经对多电梯留下了扩展接口,因此这两次作业相较于 Homework 5 在线程安全上并无较大改动——锁已经在线程安全类上做好,而多电梯的扩展并不影响已有锁的设计。

唯一需要注意的是,由于 Homework 7 中换乘的设定,可能会有 Main 之外的电梯线程调用调度器,不过由于其仅仅影响到插入操作,且插入操作中仅有线程安全类 BlockingQueue 的单条语句,因此不添加其余的语句也可以维护。

调度器设计

在三次作业中,我的整体框架如下:

2021-04-27-19-36-27.png

在设计中,电梯是一种大体上具有两种状态的物件(门打开、门关闭),因此 Strategy 类中根据电梯类提供的接口,可以根据其状态进行乘客上下、楼层移动、门口开关等操作,类似自动机一样完成转移,既好写也好维护。一种可能的代码如下:

// 1. 如果 CLOSE,先尝试能不能释放,能释放则 OPEN
// 2. 如果不能释放,则尝试能不能从 WaitingQueue 加入,能加入则 OPEN
// 3. 如果都不满足,但是电梯里有人或者运行方向上有需求,则 MOVE
// 4. 否则如果需求队列非空,则转向
// 5. 否则在此层挂起
protected void executeFromCloseOn(Elevator elevator) {
    synchronized (elevator) {
        if (elevator.hasExitingPerson()) {
            elevator.open();
        } else if (!elevator.isFull() && elevator.hasEnteringPerson()) {
            elevator.open();
        } else if (!elevator.isEmpty() || elevator.hasWaitingPerson()) {
            elevator.move();
        } else if (!elevator.isFree()) {
            elevator.divert();
        } else {
            elevator.pend();
        }
    }
}

// 1. 如果 OPEN,先尝试能不能释放,能释放则释放
// 2. 如果不能释放,尝试能不能从 WaitingQueue 加入,能加入则加入
// 3. 否则改为 CLOSE
protected void executeFromOpenOn(Elevator elevator) {
    synchronized (elevator) {
        if (elevator.hasExitingPerson()) {
            elevator.releasePeople();
        } else if (!elevator.isFull() && elevator.hasEnteringPerson()) {
            elevator.addPeople();
        } else {
            elevator.close();
        }
    }
}

对于请求分配的调度器,其中包含了等待队列,并受到调度器的控制,决定将队列中的每一个元素分配给哪一个电梯队列。电梯进程唯一地由电梯队列决定其行程,因此调度器只需着重考虑如何将请求分配给较为优秀的电梯即可。

我在前两次作业中都采用了随机分配,即将一个请求分配各随机电梯,且事实证明效果仍然不错。在第三次作业中,我通过最短路算法预处理出了所有不同楼层之间的换乘路线,并将不同电梯的换乘代价纳入其中,以获得较为真实的换乘规划。

无论是对调度器还是电梯而言,它们更像是随时接收一个输入的容器,调度器接收一个请求并分发给特定电梯,电梯接收一个请求并将其完成(或者是继续换乘,将请求拆分后重新发送到调度器中)。这样做的好处很明显:只要分别保证这两者在输入过程是线程安全的,那么整体的线程安全基本上也得到了保障。然而这样做也存在着一些不好处理的问题——共享对象和线程之间耦合较大,基本上所有操作都是线程与线程之间直接进行,而不是线程与共享对象之间进行。比较可惜的是后期重构代价较大,且当前的设计已经满足要求,故并未重构。

最终架构可扩展性分析

对于电梯本身的调度而言,我使用了 LOOK 算法,使得电梯在现有队列里采取较为合理的移动路径进行移动。对于电梯本身而言,其自身持有一个 Strategy,并向 Strategy 提供了用于调用的接口,以便对不同的调度策略,实现“仅更换策略成员就能实现不同的调度方法”,来完成对三种不同乘客到达情况的策略设计(这其实有点像状态模式了)。不过在实际应用中,我发现 LOOK 算法足以同时应对三种情况,因此虽然设计了这样的接口,却未在后期进一步扩展,但是其确实是具有可扩展性的。

同时,由于用 ElevatorInfo 管理所有电梯的参数,因此也可以很方便地对新电梯的信息进行控制(起点终点、可停靠楼层、移动速度等),对未来可能加入的新型电梯留下扩展空间。

UML 类图

2021-04-27-21-25-26.png

UML 顺序图

Main 线程的 UML 顺序图:

2021-04-27-20-35-43.png

调度器 Scheduler 线程的 UML 顺序图:

2021-04-27-20-36-22.png

可以见到 Scheduler 和 Elevator 进行了交互。

电梯 Elevator 线程的 UML 顺序图:

2021-04-27-20-37-31.png

在电梯中,主要与运行策略类 ElevatorStrategy 互相交互,而后面我们将看到策略类最终又会和 Scheduler 类进行交互(插入拆分的请求)。

运行策略 ElevatorStrategy 类的 UML 顺序图:

2021-04-27-20-40-12.png

Bug 分析

在三次强测与互测中,我均未被发现 bug,同时也并未发现别人的 bug。不过自己在本地的时候有一些并不是很好调的 bug,比如程序结束之后不能及时响应终止、电梯在运行过程中不能添加乘客请求等,这样的问题集中在第一次作业中(因为基本都是由共享对象并没有共享引起的 bug),后来集中修复了一次线程问题,完成了 debug。

另外一个比较容易出问题的是,如果每次都只在添加电梯时才创建线程,那么由于 Thread.start() 方法并不是阻塞的,因此有可能在分配请求时,给一个还没有完成初始化进入到 run() 方法的电梯添加请求,这有可能会导致线程出错,因此可以事先创建好对应量的电梯(也是我的解决方法),在需要的时候按需加入列表;或是在分配任务时保证电梯此时已经完成了初始化,进入了 run() 方法。

在本单元中,每个同学使用的调度算法与策略完全不同,因此如果对每一份代码都采取一种策略去进行 Hack,那么效率也将是及其低下的。同时本次涉及到多线程,因此需要设计相应的评测机和测试集,才能方便地对每一份代码进行检测。可以说本次的互测相比第一单元更需要进行黑箱测试,而不能像第一单元一样将所有易错点分析出来构造可能的错误样例,进而集中对这些样例集中测试。

心得体会

本次作业以电梯调度和通信作为一个系统设计,展开了对多线程交互与通信的探讨,将线程安全与线程锁这一问题放到重要的位置讨论。

可以说,本单元的重点几乎在保证线程安全上,这一目标可以由多个方向实现——采用已有的模式(生产者-消费者模式、工人模式等)、设置线程安全类、设置良好的共享对象等,这些策略都是相当于从已有的经验和约束中归纳出模型,让后人能够更好更方便地实现线程安全。可惜的是第一次设计多线程时整体架构欠佳,在后期时基本只能基于原有的架构基础上进行设计,因此在保证线程安全上需要更多的思考。这更让我充分认识到,一个良好的架构真的是需要具备极佳的扩展性和前瞻性的。

同时,和上个单元的作业相比,本单元中我的代码层次化设计有所提升,如采用可替换的策略成员实际上遵守了开闭原则,使得对任意的策略来说,都可以仅仅通过更换策略进行扩展而不影响原有的代码。但是在松耦合上做得欠佳,如调度器线程与电梯线程实际上是互相通信的,这违背了迪米特原则。一种可能的改进方法是,将共享对象提取出来交互(这也是我的架构中两线程通信的本质),以实现较松的耦合程度。

总体来说,本单元给我了诸多收益,尤其是学习到了线程通信之间的方法与原则,让我对 OO 又有了进一步的了解与认识。

posted @ 2021-04-27 21:31  nikkukun  阅读(145)  评论(0编辑  收藏  举报