BUAA_2022_OO_第二单元总结

线程安全——同步块与锁

在三次作业中,我均选用了synchronized锁。一方面,它实现简单,可自动释放,不容易出bug;另一方面,作业中的读写操作都比较简单,读读、读写、写写均互斥也不会造成太大的性能影响。

第五次作业

所有同步块均设置在共享对象类RequestQueue的方法中,所有加锁的方法均为对共享对象的读写操作:addRequest写操作;hasWork、hasTheRequest为读操作;getOneRequest、getTheRequest为读写操作。以实现线程安全访问共享对象的目标。

第六次作业

增加了共享对象ArrayList<ArrayList> electorsQueue,是所有楼座、楼层的等待队列。即对其读写操作的语句块加锁。

第七次作业

用ArrayList<ArrayList> electors替换了electorsQueue,加锁方式不变。此外,在Scheduler中增加了属性getTimes,用于计数Scheduler还应当对多少请求进行调度,对写getTimes的两个方法——addTimes、subTimes加锁。

综上所述,锁存在的唯一目的就是保护共享对象。当一个对象可能同时被多个对象操作,导致读或写操作的目标无法实现,就应当对相应操作加锁,实现互斥,保证正确性。

调度器设计

第五次作业

调度只有电梯接送对应乘客一步。
调度器作为线程出现,但仅仅完成了将需求分配到楼座队列的作用,实际上并没有调度功能;对需求的具体调度由电梯实现(采用look调度策略)。
在线程交互方面,调度器线程与读入线程有着共享对象allRequest,读入线程负责写入请求,调度器线程读取请求;调度器线程与电梯线程有着共享对象electorQueues,调度器线程将请求写入对应楼座队列,电梯线程再读取对应楼座的请求。

第六次作业

同一楼座、楼层可以有多部电梯,调度分为将请求分配给电梯和电梯接送对应乘客两步。
第一步调度采用平均分配的策略:在调度器中增加int[] num属性,用于记录每一楼座、楼层已经接收了多少请求,而后用该请求数量模该楼座、楼层有多少电梯,得到应该将当前请求分配给第几部电梯。
第二步调度中,纵向电梯仍采用look策略,横向电梯采用ABCDE转大圈的策略(相信大道至简),因为总共只有五个楼座,最坏的情况也不会慢很多,当请求较多时有着明显的性能优势,而且逻辑简单、不会出bug。
线程的交互关系与第一次作业相同。

第七次作业

出现了换乘的需求,调度分为将请求拆分、将请求分配给电梯和电梯接送对应乘客两步。
在电梯类中设置了time参量,用于估计电梯完成已有请求所需要的时间,代码如下:

/*
size为电梯容量
speed为电梯移动一层或一座所用时间
stopnum为电梯可停靠位置数目
*/
public double time() {
    int personNum = wait.size() + on.size(); //正在送和等着送的人数
    if (personNum > size) { //按电梯是否能一次送完分为两种情况
        return ((double) personNum) * (speed + 1.2 * 2 / stopNum * 400) * ((double)personNum) / size;
    }
    else {
        return ((double) personNum) * (speed + 1.2 * 2 / stopNum * 400);
    }
}

第一步调度由读入线程完成。将所有请求看作三步完成:纵向、横向、纵向(考虑多次换乘开关门时间、等电梯时间、逻辑复杂出bug)。那么拆分实际上就是找到中转楼层:若无需中转,中转楼层设为目标楼层;若可在出发楼层或目标楼层中转,选择拥有最短time的楼层;若可在出发楼层和目标楼层之间楼层中转,选择拥有最短time的楼层;否则按移动距离由小变大的顺序遍历其余楼层,能中转即选择。
第二步调度由调度器线程完成。选择该楼座或楼层time最小的电梯分配请求。
第三步调度由电梯线程完成。纵向电梯采用look策略;横向电梯先看ABCDE方向两步内有无电梯内请求要下,若无,再看ABCDE方向两步内有无电梯外请求能上(电梯不满),若无,电梯按EDCBA方向运行,否则按ABCDE方向运行。
在线程交互方面,增加了电梯线程写allRequest,让需换乘的请求得到调度器的再次分配。

基于类图与时序图的架构分析

第五次作业

采用生产者消费者模型:Input线程读入请求并放入“托盘”allQueue中,再由Schedule线程从allQueue取走经过判断放入“托盘”electorQueue中,最后由电梯从electorQueue中取走请求,电梯采用look策略运送乘客。类图、时序图如下:

第六次作业

增加了横向电梯以及过程中增加电梯的要求,整体架构没有变化。增加了横向电梯类,并在Input线程中增加了区分请求类型及增加电梯的代码。类图、时序图如下:

第七次作业

增加了换乘要求,在原有架构的基础上添加了流水线模式。在Input线程中,调用了parse静态方法,将一个请求拆成多个单步请求(仅有纵向或横向移动),用MultiRequest包装,只对外界暴露出当前需要进行的单步请求,电梯完成单步请求后将其删除,MultiRequest中需要进行的下一步操作就暴露出来,重新投喂给Schedule再次分配。类图、时序图如下:

未来功能拓展

假设可添加特殊功能的电梯:只能达到指定楼层,运载能力与人、货重量有关等。仅需对电梯的属性、运行策略等改变,整体架构无需变化。

Bug分析

第五次第六次作业没有出过大bug,下面详细讲讲第七次作业出现的三个bug。

同步块范围引起的先notify后wait

在我的调度器的run方法中,一上来先后判断要不要结束end以及wait,我给它们分别加了锁,在两个同步块之间线程可能会切出去,在线程没有wait时notify并设置满足end的条件,然而此时的条件同样会使线程wait,以致该线程再也不会被唤醒。
扩大同步块范围至包含两部分判断即可解决。

错误判断横向电梯可达范围

我开始设置了一个5*10的boolean数组,每加入一个横向电梯就把对应位置标true。这意味着当加入一个可达ACE的电梯、一个可达BD的电梯,我就会认为有一个可达ABCDE的电梯。
遍历所有电梯判断可达性解决。

横向电梯策略导致反复横跳

横向电梯看ABCDE方向两步内有无电梯内请求要下,有无电梯外请求能上,若无,电梯按EDCBA方向运行,否则按ABCDE方向运行。然而存在这样一种情况:电梯满载在B座,C座有请求,电梯内请求都在A座下。电梯就会在BC座反复横跳,上不来下不去。
电梯满载时不考虑电梯外请求即可解决。

hack策略

在本单元的测试中,我主要采取的策略是随机数据生成+bug点特殊测试的方式。
​在第五、第六次作业中,选择的策略都是在同楼层或同楼座同时投放大量请求进行测试(第六次可添加多部电梯)。
​在第七次作业中,换乘的正确性是测试的重点,因此先在允许范围内在多个不同楼层尽量多增加横向电梯,然后在同一时刻大量投放必定要换乘且有多种换乘楼层选择的请求。此外,我根据自己在上面的Bug分析中提到的第二个bug,构造了一个三行的测试样例,一刀三人,爽!!
​在互测中,我每次都能刀到三四个人,可能是运气比较好,房内的同学多少都有bug。

​关于线程安全相关问题,我的测试策略是同一数据多次运行或者大量随机数据测试,以量取胜。没有想到特别的构造方法。

​本单元和第一单元测试的较大差异之处在于结果的正确性难以确定,错误的情况很多,评测机不好写。我因为懒一直没有搞评测机,导致只能对着代码debug,跑完样例也不知道对不对,十分痛苦。

心得体会

线程安全

因为是第一次接触多线程,写第五次作业让我非常痛苦,靠着CSDN、与同学讨论以及其它资料,几天时间内基本弄明白了多线程到底是什么、什么是线程安全,学会了锁、notify-wait机制,多线程由陌生变得有趣。在第六次作业又了解了lock读写锁以及锁在底层是如何实现的,让我在解决线程安全问题时有的放矢。
我认为在学习某样新东西时一定要具体写代码,完成一个任务时会很有成就感;浮于理论只会造成眼高手低。

层次化设计

我认为这一单元的层次化设计体验不如第一单元来的明显,有点像CPU的流水线结构,每个部件做好自己的功能即可。此外,在迭代时,整体架构基本没变,每次作业都是在之前基础上缝缝补补,也可能是没有第一单元痛苦的重构经历,没有让我感受到架构设计重要性。

posted @ 2022-05-01 15:39  现充宅  阅读(36)  评论(0编辑  收藏  举报