面向对象设计与构造第二单元总结

作业分析

第五次作业

题目简述

使用多线程模拟五座楼中每座一个电梯的运行、开关门、上下客。

电梯能到达 1-10 层,所有电梯人数、运行速度固定。

思路简述

设置一个输入线程,一个调度器线程,五个电梯线程。

输入线程读入请求,送到调度器,调度器根据楼座分配给电梯,电梯内部根据特定策略模拟行为。

具体来说,电梯运行策略为:先向一个方向移动,到达一个楼层后如果有人要下去或者有人要上来(上来需要满足目的地方向和当前运行方向一样,且电梯不为空),就会开门然后先下后上。

判断方向的逻辑为,如果当前电梯里是空的,并且运行方向前方没有可以接的请求,就会掉头。否则继续前进。

电梯内每个楼层都有一个容器来存放以这个楼层为起点的请求。

程序分析

UML类图

hw5类图

UML协作图

hw5协作图

同步块和锁

程序中只采用了生产者——消费者模式,输入线程和调度器线程中有一个等待请求队列的共享对象,调度器线程和每个电梯线程之间也各有一个。

那么主要的线程安全问题就是在这个共享对象中。

这个请求队列是自己建的一个满足线程安全的类,可以满足放、取请求,设定结束。于是为了满足线程安全,基本在每个方法上都加了一个锁。类似于第三次课上实验代码中的框架。

public class RequestQueue {
    private final ArrayList<CustomRequest> requests;
    private boolean isEnd;
    
    public RequestQueue() {
        requests = new ArrayList<>();
    }
    
    public synchronized void addRequest(CustomRequest request) {
        requests.add(request);
        notifyAll();
    }
    
    public synchronized CustomRequest getRequest() {
        if (!isEnd && requests.isEmpty()) {
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        if (requests.isEmpty()) {
            return null;
        }
        CustomRequest request = requests.get(0);
        requests.remove(0);
        notifyAll();
        return request;
    }
    
    public synchronized void setEnd(boolean isEnd) {
        this.isEnd = isEnd;
        notifyAll();
    }
    
    public synchronized boolean isEnd() {
        return isEnd;
    }
    
    public synchronized boolean isEmpty() {
        return requests.isEmpty();
    }
}

此外,官方包中给的输出并不保证输出是线程安全的,因此还需要一个线程安全的输出类,对输出加锁来确保线程安全。

public class OutputHandler {
    public static synchronized void print(String msg) {
        TimableOutput.println(msg);
    }
}

调度器与交互

这次作业中调度器的功能比较单一,只需要把输入的请求按照楼座分配给对应的电梯即可。

只要调度器的等待队列不为空,就会读取请求并扔到对应电梯的队列里。

直到输入完毕,调度器的等待队列设定了 isEnd 并且已经分配空了,那么此时调度器也应该结束工作,给每个电梯的队列设置 isEnd。

电梯的停止逻辑为队列输入结束并且空了,还有电梯内部的请求也空了。

为了避免轮询,在输入没有结束但是队列为空的时候调度器和(内部请求已经处理完毕的)电梯都需要 wait()。

Bug分析

强测中通过了所有测试点,获得 99.6278 分。

互测中没有被发现 bug,找到了 2 位同学的 3 个 bug:

  • 1、输出线程不安全

  • 2、调度有问题,电梯运行方向为目前电梯里离目的地最近的一个请求的方向,并且每次上人没有考虑方向。构造出一个数据使其电梯反复横跳。

第六次作业

题目简述

使用多线程模拟五座楼中电梯的运行、开关门、上下客。

电梯有纵向横向之分。

纵向电梯能到达某一座的 1-10 层,横向电梯能到达某一层的 A 到 E 座,并且横向是一个环。所有电梯人数、运行速度固定。

初始每座楼有一个纵向电梯,过程中可以新加两种类型电梯。

乘客的起点和终点要么楼座相同,要么层数相同。

思路简述

我的做法是把横向纵向电梯统一起来,每楼的每层用一个独立的编号代表,称之为“单元”,例如 A 座 2 层为 1 号,B 座 3 层为 12 号。当时的想法是这样实现可扩展性比较好。

每个电梯用一个 ArrayList 记录能到达的单元,那么区别就是是否循环。找下一个的时候分类讨论一下即可。

由于乘客的限制,不需要换乘,只需要判断横向还是纵向然后分配给对应的电梯即可。判断可达性和电梯种类没有关系。

横向电梯采用的策略是一直往一个方向转。

新增电梯就在调度器里新建一个线程开始 run,也没啥特别的。

其余地方和第五次作业相同。

程序分析

UML类图

hw6类图

UML协作图

hw6协作图

同步块和锁

和第五次作业完全相同。

调度器

调度器的内容和第五次作业差不多,区别在于有增加电梯的请求。那么在处理输入线程传进来的请求的时候需要判断是哪种请求,如果是电梯就要加一个线程。

分配电梯时可能会有多个,我选择直接随机一个可以到达目的地的电梯分配进去。

Bug分析

强测中通过所有测试点,获得 97.1709 分。互测中没有被发现 bug,也没有找到其他同学的 bug。

第七次作业

题目简述

使用多线程模拟五座楼中电梯的运行、开关门、上下客。

电梯有纵向横向之分。

纵向电梯能到达某一座的 1-10 层,横向电梯能到达某一层的 A 到 E 座的 \textbf{某些}座,并且横向是一个环。所有电梯人数、运行速度可以定制。

初始每座楼有一个纵向电梯,第 1 层有一个可以到达每一座的横向电梯。过程中可以新加两种类型电梯。

思路简述

在第六次作业的框架下,可以很方便地把每个单元抽象成点,然后建图,可以跑一个最短路,通过这个最短路到达目的地。

跑出来最短路之后,相当于把整个路线分成若干段,我在每个请求里用一个 ArrayList 存储换乘点,每次到达当前阶段的换乘点或终点的时候就出电梯。然后继续下一个阶段。

为了优化一些性能,我用电梯的速度、换乘次数以及电梯当前的人数和容量来做一个时间的估价函数,用这个来跑最短路。

横向电梯不是都能到达,但是每到达一个单元又要输出,这与我的架构有一点矛盾,只能仍然按照一步一步来枚举到下一个可以到达的单元。

程序分析

UML类图

hw7类图

UML协作图

hw7协作图

同步块和锁

在做最短路的时候,我需要知道电梯当前的人数来估计时间,但直接读取会有冲突的问题,因此我额外增加了一个共享对象,存储当前的人数,并实时更新。在加减人数和询问人数的时候都需要加锁。

调度器

同样和第六次作业差不多,增加了做最短路和分阶段的功能,一个阶段完成之后由电梯继续塞回调度器的等待队列,然后由调度器分配给下一个阶段。

Bug分析

强测中出现了大问题,通过 5/20 个测试点,得到 24.9405分。

由于强测爆炸,互测当中随便扔了个数据就 hack 成功了 4 位同学。自己没有被找出 bug。

实际上我的 bug 在于题目理解有误,以为横向电梯初始停在A座且A座能开门,所以把横向电梯的初始位置设置成第一个能开门的位置。这样碰到 A 座不能开门的情况就会出错。

解决方法是让横向电梯初始在A座,在开始运行之前使其先跑到第一个能开门的位置,再正常运行。

测试策略

手动测试

多线程的 debug 比第一单元的单线程复杂很多,因为不能单步调试,而且错误很难复现。

我认为一个比较好的测试方法是:先测试只有一个电梯在工作的情况,是否能够正常运行。确保能够正常运行之后再测试多个电梯一起运行的情况。这样能够逐步排查是电梯逻辑问题还是多线程交互问题等。

此外,轮询也是一个常见的错误,最简单的方法是看任务管理器的资源占用率,此外还有插件等方法。也可以构造一个比较多请求的数据,自测一下 CPU time,来确保没有轮询。

自动化测试

数据随机生成即可,有一定强度。

难点在于判断是否合法,也是一个大模拟,需要判断各种输出信息的时间是否合法,人是否都到了目的地,电梯是否超载……

Hack 策略

基于自动化测试的策略就没什么可说的了。

手动 Hack,观察同学的代码,主要侧重于几个方面:调度策略是否有问题,是否能及时结束,是否会轮询……这些是比较容易出现的错误。

心得体会

通过本单元的三次作业,我对多线程交互有了初步的认识。在作业中我实现了生产者——消费者模式,而在实验中我也了解到了主从模式和流水线模式等。这些模式都各有优点,需要掌握并灵活运用。

生产者——消费者模式的层次是很明显的,生产者、传送带、消费者,在作业中也就是输入线程、请求队列、调度器和调度器、请求队列、电梯两个情况。在分析确定了层次之后,就可以考虑实现不同层次之间的交互了。

在线程安全方面,首先要确保的就是共享对象的加锁,比如作业中我实现的请求等待队列,在取请求、放请求等操作的时候都要加锁,否则有可能出现问题。

我只使用了在方法前面加 synchronized 关键字的加锁方法,这种方法是最简单的,也足够适用于这三次作业。在课堂上我了解到,实际上还有多种其它的加锁方法,以及 synchronized 控制同步块的加锁方法,这些方法各有优势,还需要我以后不断探索。

为了防止轮询,在没有请求的时候需要 wait(),那么就要在恰当的地方用 notify() 或 notifyAll() 来结束 wait(),避免死锁。比如放入请求和设置输入结束后都需要 notifyAll()。查询的时候也要注意条件判断,第七次作业在加入换乘的时候我遇到过轮询的问题,因为询问的时候需要判断乘客是否换乘完毕。如果输入结束了但还没换乘完毕,是需要 wait() 的,而不能直接返回一个 null。

以上就是我在这个单元后对多线程的一些理解。其实对于作业来说,调度策略不是最重要的,只要没有明显错误,效率并不会差太多。而通过电梯和调度器的交互理解多线程才是作业的目的。


我在实现第五次作业的过程中,一开始也遇到了不少困难,在多线程的交互上有一些疑问。后来我仔细分析了不同线程的作用和需要进行交互的地方,并且参考了生产者——消费者模式,把作业的任务套入框架之后,实际上架构和原理还是非常清晰的。

在第六次作业,尽管加入了横向电梯,但并不会换乘,所以实际上和第五次差别不太大。为了后续更好地迭代,我把电梯的运行路线统一了起来,抽象成一个图,进行了一点小重构。在此基础上,第七次作业完成的很快,并且实现了多次换乘,可以实现更复杂的要求。可惜最后要求并没有那么严格,有点杀鸡用牛刀的感觉。

此外,最后一次作业的大失误也是我始料未及的。因为我对题目理解有误导致电梯的初始位置出错,中测中没有出现问题。而我按照自己的理解来进行测试,自然也查不出错。只能说自己确实不够细心,比较可惜。不过还是想吐槽一下,感觉第二单元的中测强度确实相比于第一单元大幅度减弱了。尤其是第七次作业,在和同学交流的过程中我了解到有一些同学的运行逻辑有巨大问题,却直接通过了中测,结果就是强测几乎没有拿分。

posted @ 2022-05-02 15:33  Oshwiciqwq  阅读(80)  评论(1编辑  收藏  举报