引言
面向对象程序设计第二阶段的三次作业已经结束。此阶段的三次作业均是基于目的选层电梯的电梯系统设计,也是第一次关于JAVA多线程的阶段性作业。
第一次电梯作业
作业简介
基于先来先服务调度策略控制单部电梯进行人员运送。在评测中有时间限制,但无性能要求。
设计策略
线程模型:生产者-消费者
-
共享对象
第一次作业中,有两个线程——请求输入线程、调度器线程,输入线程与调度器线程共享一个电梯请求队列,用来同步消息。电梯此次并未被设为线程,而是被调度器直接指挥。这次作业使用的是生产者-消费者模式,输入线程作为生产者将用户请求加入请求队列,调度器线程作为消费者处理请求队列中的请求并根据请求指挥电梯运行。
-
线程安全的容器:阻塞队列
在此次作业的第一个版本中,笔者使用的容器是List。因为List不是线程安全的容器,笔者在对容器List进行添加、访问、删除的方法中都通过synchronized来解决多线程读写冲突的问题。
但是对于java这种面向对象语言,有很多封装好且线程安全的容器可以让java编程者使用,通过查看选择了线程安全的阻塞队列LinkedBlockingQueue作为输入请求线程、调度器线程的共享容器。 -
对请求进行封装
为了在得到输入结束信息后,通知所有线程请求结束指令已经到达,笔者在此次作业中对请求指令进行了封装,通过标志位来判断当前指令是不是请求结束指令。其实这种封装可以,但没必要。在第二、第三次作业中这种封装被取消了。
public class RequestStruct { private PersonRequest oneRequest; private boolean status; public RequestStruct(PersonRequest oneRequest,boolean status) { this.oneRequest = oneRequest; this.status = status; } public PersonRequest getOneRequest() { return this.oneRequest; } public boolean getStatus() { return this.status; } }
调度算法:先到先服务
笔者在此次作业按照指导书实现了先到先服务调度算法,笔者在使用阻塞队列作为共享容器后,输入线程只需使用put方法将请求加入到队尾、调度器线程只需使用take方法从队首拿出请求。
线程间同步方式
-
使用阻塞队列实现线程同步
java线程同步方式有很多,譬如使用synchronized实现方法、代码块同步;使用特殊域变量(volatile)实现线程同步。但是上述举例中的同步方式都是在底层实现的线程同步,但是我们在实际做java开发当中,应当尽量远离底层结构。笔者在这次作业使用的是阻塞队列LinkedBlockingQueue作为线程共享容器实现线程间同步。
度量分析
复杂度度量分析
CK度量集分析
本次作业协作图
本次作业的类图
从类图、复杂度、CK度量集分析架构设计优劣
从类图上来看,类的关联是按照请求处理流来设计的,如此设计可以保证无多余的类间关系,但是在之后作业由于不同电梯处理请求的能力不同,请求处理流会出现分支,而此次作业的设计对于此类增加的需求可扩展性不强。
从上述的复杂度分析、和CK度量集分析此次作业的各个类耦合度不高,各个方法的圈复杂度也不高(自己在第一阶段的作业中过多用if-else结构,导致圈复杂度过高,在此次作业中努力去减少控制流的分支数)。因为本身作业难度不大,内聚程度的优劣没有被展现出来。
基于SOLID设计原则检查项目设计
-
单一职责设计原则(SRP)
此次作业的设计基本上符合单一职责原则,电梯、调度器、输入线程的工作都是分开的。
-
开闭原则(OCP)
因为所有类在这次作业都只被实例化一次,故未使用抽象工厂或接口来进行对象创建,此次设计在这方面仅仅是满足了一次完成作业的要求。
第二次电梯作业
作业简介
此次作业依旧是单部电梯的多线程作业,而该电梯具有捎带功能。评测时有时间要求,且含性能分。
设计策略(从多线程的协同和同步控制方面)
线程模型:观察者模式
-
观察者模式
在此次作业中,笔者使用的是观察者模型。对于输入线程,调度器是观察者,输入线程是消息发布者;对于调度器线程,电梯是观察者,调度器是消息发布者。当有无新的请求时,调度器和电梯线程处于阻塞状态,避免了暴力轮询而导致CPU运行时间过长;当有新的请求到来时,消息发布者会update消息,通知观察者消息有更新。
调度算法:look调度算法
此次作业要求增加捎带功能,评测时以ALS调度为时间基准,但指导书鼓励同学们多尝试其他调度算法,于是笔者复现了look调度策略。
线程间同步方式
此次作业没用阻塞队列作为线程安全容器,而选择了Vector,主要是因为笔者想实现观察者模式,所以选择更麻烦的方式,如上文言,运用观察者模式来实现线程间同步,观察者未被唤醒时一直wait,而消息发布者更新后,观察者被唤醒,拿到锁后去处理请求。
度量分析
复杂度度量分析
CK度量集分析
复杂度过高原因分析
基于上表,主要还是电梯运行的软件复杂度和圈复杂度过高(我写的java代码又圈复杂度过高了),主要是因为自己把控制流不断分支来实现look调度算法而导致的,如果我复现的是scan调度算法可能会圈复杂度低一点,但是这样在电梯的乘客可能会等待时间更长些。而软件复杂度过高的原因是因为,电梯运行时的将各个本可集中在operation()方法内的操作,过多地拆分了出去。但是这样的拆分其实对于笔者而言,在写码过程中的维护还是有益的。
本次作业协作图
本次作业的类图
从类图、复杂度、CK度量集分析架构设计优劣
从类图可以看出,架构还是沿用第一次基于请求处理流来设计。从CK度量集分析来看,各个类的耦合度不高,这种设计还是有设计简单,耦合度低的优势;有拓展性弱的劣势。
基于SOLID设计原则检查项目设计
单一职责原则依旧是满足的。因为本次作业中没有继承子类和接口的应用,故LSP、ISP、DIP原则也不适用于此。
第三次电梯作业
作业简介
此次作业需调度三部电梯,各个电梯的性能和可达楼层不同,性能包括运行速度,可载客数。
设计策略
线程模型:观察者模式
-
观察者模式
线程模型沿用了第二次作业使用的观察者模式。
-
线程安全的容器
容器沿用了第一次作业的阻塞队列。
调度算法:多级优先队列+look算法
此次作业维护多级优先队列的目的是为了防止多电梯争抢指令以及一个电梯在繁忙状态而其他电梯可以运人时却处于空闲状态的情况出现。简单来说A电梯会依次去只能由A电梯来运送的请求队列;可以由A、C电梯运送的请求队列;可以由A、B电梯运送的请求队列;可以由A、B、C电梯运送的请求队列去处理指令,访问次序反映的是请求的优先级。对于B、C电梯类似。
对于不可由一部电梯完成的请求,此次作业先让多部电梯(两部即可)中的第一部电梯执行指令,将用户送到过渡层;再由该电梯构造一条新的请求传入调度器。
维护多级优先队列是由调度器来完成的,而各个电梯具有自主权,其按照look调度算法运行。
度量分析:
复杂度度量分析
CK度量集分析
复杂度过高原因分析
为了构造优先级队列,此次作业我需要一些条件判断来将指令添加到这条指令应在的队列中,所以allocateRequest方法复杂度高;电梯依次要去各个队列访问指令,所以电梯的anyIn方法复杂度高。
本次作业协作图
本次作业的类图
从类图、复杂度、CK度量集分析架构设计优劣
此次作业设计还是开了三类线程:输入线程、调度器线程和电梯线程。有大佬将用户设为线程,用户用贪心算法来决定上下,在保证电梯自主性的同时,也赋予了用户自主性,当时听到这样的设计还是挺惊讶的,这样的设计对于三次作业都是按照请求处理流来设计架构的笔者来说还是挺新鲜的。
基于SOLID设计原则检查项目设计
-
里氏替换原则
LiftA、LIftB、LiftC继承自Lift,三个子类电梯有能力出现在父类出现的地方。
-
单一职责原则
这次为了不由调度器拆分指令,
这样我就不用构造等待队列或者构造其他数据结构了,电梯具有了构造请求并像调度器发送请求的功能,这不符合单一职责原则。
Bug分析:
强测环节
这三次作业都经过了本地评测姬大量随机数据的测试;自己也构造了一些边界测试数据和极端测试数据对作业进行了测试。为了确保正确性,这次在优化上是依照现有的调度算法再稍加改进,可靠性得到了保证。所以三次作业在强测中未被找出bug,但是性能分也中规中矩。
互测环节
-
这次互测的策略与第一阶段作业的互测策略有所不同,并且找bug的难度更高,第一阶段作业读正则表达式、读求导函数可以发现bug;可能有些同学对字符串的操作不正确也会导致bug,但是这次作业输入输出都是使用官方的接口,只能从调度算法和代码逻辑入手来hack。
此次作业我并没有直接通过读代码来找bug,而选择先进行黑盒测试,如果有测出的bug,再去看该代码是哪里出现了问题。 -
写在后面
在第二次电梯作业互测环节,起初房间一片0/0,但是助教预警有很多c房间还没有提交样例,互测开始第二天中午我在评测机上用一组随机样例跑了下发现我们组其中一个人多个样例会输出错误,
当时自以为C组无疑。对于屋内这位成员的第二个同质bug,我提交了4个样例。在第二周公布身份时,并未在两个微信群找到这位同学的微信,只能在这表示歉意!
此阶段作业的收获与展望
-
学习了线程同步方法
在这三次作业及上机实验中,学习到了生产者-消费者模式、观察者模式和监听器模式进行线程同步。这些线程同步模式大大方便了我去思考如何对问题进行抽象和分解,而且这些线程模式的耦合度非常合理。
-
学习了多线程安全的容器和方法
从刚接触多线程、并发运行到自己实现安全的多线程作业,笔者在确保线程安全方面得到了锻炼,今后自己在确保线程安全的前提下,还需要进一步在提高程序性能的方面努力。
-
学习了SOLID设计原则
这三次作业中,笔者尽力遵循了SOLID设计原则。笔者在实现中发现,遵循这五个原则的代码,代码逻辑更清晰,更易读;可扩展性更高。在确保这些设计原则的过程中,对于写码者的写码能力和设计能力都有所锻炼。
-
总结了降低圈复杂度和代码耦合的方法
- 不要抽象出浅层类。因为浅层类大多数方法都是直接调用成员变量的方法,对项目问题的分解没有帮助却增多了代码间的耦合。
- 不要将枚举出的所有情况直接用于条件判断。首先这个做法说明代码逻辑不好。可能将条件进行分类,将公共模式提取就可以减少控制流分支数。