OO第二单元总结——多线程电梯
设计策略分析
电梯1:
任务:单部多线程傻瓜调度(FAFS)电梯
实现:3个线程,单例模式
Thread1:主线程
Thread2:输入处理线程
Thread3:电梯运行模拟线程
构建一个共享对象,用于put、get请求,该对象全局唯一,被Thread2和Thread3共同拥有,给该对象的方法加同步锁synchronized,以确保请求队列的安全。
通过wait、notify,使电梯和输入处理线程交替进入共享对象的临界区执行。
电梯模拟行为:从队列取一个请求,获取其fromFloor、toFloor、personId,一次只进入一个请求、将该请求完成后再进入临界区获取新的请求,当取得请求为null时,表示没有请求了,结束电梯线程。
电梯2:
任务:单部多线程可捎带调度(ALS)电梯
实现:总体架构与第一次没有太大变化,只是改了调度器的调度方法和电梯相应的请求处理行为
调度器主要method:
mainGet():返回请求队列第一个元素,传给电梯,作为其主请求;
passGet():传入电梯的当前楼层和其主请求,返回可捎带请求队列;
nobody_passGet():传入电梯当前楼层和其主请求的fromFloor,返回电梯接主请求的途中可捎带请求(相当于一个小优化);
电梯requestHandler():电梯为空时,与调度器交互获得主请求,然后从当前楼层出发前往主请求的fromFloor,途中在每一层调用nobody_passGet(),在主请求进入电梯后,执行主请求,在每一层调用passGet(),并且在每一层进行判断,调整主请求为电梯乘客队列中的passger.get(0)。
电梯3:
任务:多部多线程智能(SS)调度电梯
实现:电梯架构依然没有太大变化,仍然是单例模式。
对于每个电梯,采取ALS策略。
给PersonRequest添加一个bool变量canIn,true代表活跃(可进入电梯),flase代表休眠(暂时不可进入电梯),其作用为:由于特殊请求会换乘,因此调度器会将这类请求进行拆分成两条请求,前请求进入电梯,并将后请求的canIn设为false并留在请求队列,在前请求到达换乘楼层并出电梯后,激活后请求(即将后请求的canIn设为true)。
OO度量
电梯1:
UML
Complexity metrics
电梯2:
UML
Complexity metrics
电梯3:
UML
Complexity metrics
度量分析:
前两次作业的复杂度都较低,第三次作业中调度器类的总循环复杂度和平均循环复杂度都较高,用于拆分请求的方法复杂度也很高,主要原因是该method中用了大量的多重嵌套的条件控制语句和循环。
设计原则分析:
由于没有使用接口,只继承了thread类,此处不谈LSP和ISP;
基本上三次作业基本符合SRP原则,每个类或方法都有一个大致明确的职责,但其实还能进一步明确各方法的职责;
在第一次电梯的基础上,只需扩展调度器的调度方法,即可实现ALS调度,可见符合OCP原则;
在第三次作业中,电梯类和调度器类的交互是依赖于抽象类实现的,并没有依赖于电梯类的具体对象进行交互,因而满足DIP原则。
分析自己程序bug
前两次作业没有bug,但是第三次作业在强测中几乎爆零,这个bug是由于思维定式,每部电梯在每层楼都和调度器进行了交互,因而导致电梯在不能停靠的楼层进行了开关门动作,增加一条判断语句即可修复该bug。
分析他人程序bug
很显然,本单元由于涉及到了多线程,又由于输入的定时投放问题,传统的hack策略已经不奏效了,所以我使用评测脚本来实现定时投放,并对其结果进行自动化测试。
第一单元的作业,测试数据很容易构造,输出只有一个表达式,对于不太长的数据,基本可以通过肉眼判断其正确性,但电梯作业的输出是十分长的,尤其是第三次作业各电梯的输出相互交叉,难度更大。另外,电梯作业的bug可能很难复现,有时候评测机也会误判。
心得体会
本单元设计到了多线程,而多线程很重要的一点就是其线程安全,也是多线程的难点之一,通过对共享对象的方法加同步锁来实现临界区互斥,利用wait()、notifyAll()来调节线程进进入临界区的顺序,当然还有更多的互斥机制,可以弥补synchronized的一些缺点,例如高并发下会损失效率。这三次作业我都采用了单例模式,这使得我的三次作业架构基本相同。在设计层面上,尽量符合SOLID原则,可增强代码的拓展性、复用性。这三次作业,不仅使我接触了多线程,也让我对OO设计层面有了更深的理解。
最后就是,针对我第三次电梯作业大翻车的情况,告诫自己,千万不要面向数据编程!!!