2019北航面向对象第二单元总结
本单元的重点是多线程的设计与控制。通过对java多线程知识的学习与练习,我初步掌握了多线程设计的一些要点。
一、设计策略
1.1 第五次作业设计策略
第五次作业为单部电梯多线程傻瓜调度,要求按照先来先服务的原则进行服务。首先建立一个输入类,专门用于处理输入。然后建立一个电梯类,用于模拟电梯的运行。两个线程类共享两个变量:一个是我定义的状态类实例overClass(定义了同步方法),用于判断输入是否结束,另一个为请求队列。在输入线程中,一旦读入一个请求,就先用synchronized对请求队列加锁,若读入的为null,则唤醒等待的电梯线程,并且将overClass设置为over;若读入的是请求,则在请求队列加锁后加入队列,并唤醒电梯线程。在电梯线程中,预先定义开关门的方法openAndClose()、楼层间运动的方法move()以及上下电梯的load()以及unload()方法。当请求队列为空时,则进行等待;若请求队列不为空,则按照顺序执行,执行完后便清空队列。最后在主线程中启动这两个线程。
1.2 第六次作业设计策略
第六次作业为单部可捎带电梯调度。第五次的架构已不适用于这一次作业,事实上,在上一次的策略中,请求队列中请求的个数只能保持0或1,很难读入多个请求。这是由于每读入一条请求后,输入线程刚释放请求队列的锁,就会被电梯线程抢走。本次作业在原有的基础上,增加了辅助输入线程类和调度器类。辅助输入线程类借用讨论区的一句话说,就是为投放时间十分接近的几条请求,建立一个共同的通道。输入类需要不断读入请求并加入请求队列,而在辅助类中,经过一个短暂的sleep()后,使用requestList.notifyAll()唤醒所有需要请求队列的线程。与上次作业的不同之处在于,唤醒的操作在辅助类中进行。调度器和电梯共同维护共享变量主请求和捎带请求队列。维护的算法与指导书所提出的类似,只是在可捎带请求的判断条件上略有不同,但对性能有较大的提升。电梯类中上行和下行的操作细化到每一层,电梯根据主请求的方向运行,每到达一层,自己先对主请求和捎带队列维护,然后与调度器进行交互,进一步维护主请求和捎带队列。
1.3 第七次作业设计策略
本次作业为多部电梯。考虑到捎带电梯的复杂性和较差的性能,本次我采用的算法为Look算法。在本次,保留了输入类和辅助输入类,处理逻辑和第六次作业相同,不同之处在于调度器不再是线程类,而是作为一个普通的对象被三个电梯共享。调度器负责将请求队列中的请求分配给三个电梯的请求队列中,每个电梯维护自己的请求队列,并且根据自己的请求队列设置方向和进行运动。每当电梯到达一层后,调用调度器对象的yield()方法,更新自己的请求队列。第七次作业与前两次有一个很大的不同在于,各电梯都有自己专门停靠的楼层,对于有些请求,不能通过一部电梯直接到达,需要拆分成多个请求。我采用的拆分策略是拆分成两个请求。先找出三部电梯两两之间重叠的楼层,作为换乘的楼层。起步的电梯要能在fromFloor停靠,换乘的电梯要能在toFloor停靠,在多个换乘方案中,找到经过总楼层数最少的方案作为最终的换乘方案。
二、基于度量分析自己的程序结构
2.1 第五次作业
2.1.1 类总代码规模
该次作业由于要求较为简单,实现起来较为容易,只要144行便能完成。
2.1.2 复杂度分析
从图中可以看出,电梯线程中的run()方法ev(G)较高,非结构化程度较高。这是由于在run中对请求队列进行了遍历,并且进行了过多的条件判断造成的。
2.1.3 类图
本次作业一共建立了四个类:输入类、电梯类、主线程和结束标志类。从类图可以看出,各类之间没有过于复杂的关系,架构较为简单清楚。
优点:各类的功能分类清楚,架构清晰。
缺点:在电梯线程类中的run()方法复杂度较高,应进一步模块化。
2.1.4 线程协作关系
2.1.5 根据SOLID分析存在的问题
OCP(开放封闭原则):软件实体应该是可扩展,而不可修改的。也就是说,对扩展是开放的,而对修改是封闭的。然而从第六次作业来看,本次作业的架构还是太过naive了一些。对于电梯类运行的方法显得过于简单粗暴,例如从1层到10层是直接调用move(1,10),而后面的作业需要在每一层与调度器进行交互,这样不可避免的需要更改运行的方法,改成goUp()以及goDown。在OCP原则上,本次作业的扩展性较差。
2.2 第六次作业
2.2.1 类总代码规模
ControlClass:
ElevatorClass:
FileInputClass:
InputClass:
MainClass:
整个工程的代码总规模为:
相比于上一次作业,有了大幅的增加。但各个类的代码行数控制得较好,并不显得臃肿。
2.2.2 复杂度分析
由复杂度分析可以看出,调度器中,对于捎带请求的判断以及维护主请求和捎带请求的方法显得模块化程度低,复杂度较高。这是由于判断捎带请求的条件过于复杂导致的。而run()方法过于复杂,还是因为模块化程度不够,应当尽量将run()中的过程局部化和模块化。
2.2.3 类图
(图片过大,建议在新标签页中打开并放大,亲测清晰)
本次作业的类图较为复杂,其中FileInputClass其实是用于本地模拟定时投放请求。主要的类只有四个:电梯类、输入类、辅助输入类以及调度器类。
优点:各类有了清晰的分工,相互间的耦合度低,有利于拓展。
缺点:部分类中run()方法复杂度较高,难以维护和debug。
2.2.4 线程协作关系
2.2.5 根据SOLID分析存在的问题
SRP(单一责任原则):让一个类只做一种类型责任,当这个类需要承当其他类型的责任的时候,就需要分解这个类。而在本次作业中,输入类和辅助输入类承担的是单一责任,然而调度器和电梯需要共同维护主请求和请求队列,同一种功能需要两个类共同承担,在这一原则上履行得不好。
OCP(开放封闭原则):由第七次作业的过程可以体会到,我的电梯类和调度器类都进行了重写,这是因为采取的调度策略不同,电梯和调度器需要共同维护的变量就不同了。最理想的方法是建立一套电梯运行的指令,由调度器根据不同的策略生成指令交给电梯,而不需要电梯自己进行一定的调度。
2.3 第七次作业
2.3.1 类总代码规模
本次作业代码规模与上次差不多,对调度器类有了较大的改动,其余均为小改。
2.3.2 复杂度分析
总体复杂度不高,但和前两次作业一样,在run()方法中,代码的复杂度过高,在今后的线程类中要注意做到尽可能高的模块化。其余复杂的部分在于电梯运动方向的决策,以及分配请求上。这部分由具体决策决定,难以避免。
2.3.3 类图
(图片过大,建议在新标签页中打开并放大,亲测清晰)
优点:从整体来看,做到了高度的模块化,各类行使的功能很好地区分开。上一次的主请求和捎带队列是由电梯和调度器共同维护,而这一次每个电梯的请求队列由电梯和调度器共同维护。与上一次相比减少了一个线程,使得线程的控制更容易。
缺点:三次作业线程的run()方法写得都不够简洁,这是我需要注意的地方。
2.3.4 线程协作关系
2.3.5 根据SOLID分析存在的问题
SRP原则:满足,各个模块的功能基本都是分开的。
OCP原则:本次作业虽然相比前两次有了很大的提高,但是仍然在扩展性上有缺陷。在调度器类中,专门针对三种电梯建立了三个不同的队列,但若扩展到100个电梯,这样的编码就显然不适用了,需要专门建立一个标准化的调度器类才行。
LSP和ICP均满足,未实现继承和接口。
三、分析自己的bug
第五次作业中测和强测均没有bug。
第六次作业中,强测报了一个bug,评测机给出的信息是运行时间过长。输出结果上看是正确的,但在评测时应该是由于notify先于wait导致线程没能正常退出。本地测试了多次均没有复现这个bug,猜想是本地的测试在时间上控制得不够精准。这属于线程安全控制的问题。
第七次作业让人十分心痛,虽然通过了中测,但强测大面积崩盘。这是由于没有仔细阅读指导书导致的,指导书要求在任何时刻都不能超载,而我的三次作业中,电梯开关门期间都是先进后出。前两次是因为没有限制便自然地在openAndClose()里先调用Load()再调用unload(),第三次就直接用了第二次写的openAndClose()方法。这就导致,虽然进来和出去的人员是确定的,但由于顺序错了,直接超载。
四、发现bug的策略
因为高工课程没有互测环节,于是便讨论是如何发现自己的bug的(虽然眼瞎没全部发现)
首先用python随机生成请求,然后自己写了个FileInputClass用于本地文件的定时输入。为了方便,我是在程序内部增加了一些用于check的代码。例如记录每个电梯进入的人员序列checkin和每个电梯出去的人员序列checkout,进行比对;然后将三个电梯的人员序列进行去重合并后与输入的请求序列进行比对,若完全相同,则表示所有的人都送到了目的地。非常遗憾的是,由于我对自己防止超载的措施十分自信,因而只简单地输入了8个出发楼层为1的请求,看看A电梯确实只上了6个,就放心地由他去了,没能在每个IN时检查电梯人数。从而酿成大错。
为了检验自己的控制逻辑(如拆分请求、更改方向),我专门设计了一些样例。通过这些样例,我对整个运行过程的认识也愈来愈全面。
五、心得体会
5.1 线程安全
在线程安全上,最困难的是刚开始接触多线程的第五次作业。虽然逻辑很简单,但在当时对wait和notify的机制尚没有完全明确,也不知道如何结束所有的线程。好在研究清楚wait和notify机制之后,已经能够得到正确的输出,但还是没有办法每次都安全退出。后来通过一个overClass的共享,使得电梯线程在执行完所有任务之后能够安全退出。后面的第六、七次作业则是更加巩固了对线程安全的认识。在写多线程程序之前,就要预先考虑好所有的线程间的联系。毕竟多线程程序的debug还是非常痛苦的……
5.2 设计原则
相比于第一单元的次次重构,本单元有了很大的进步。代码的复用性也越来越高(虽然第七次作业的爆炸是因为复用了前面作业的代码)。但在功能划分和模块化上仍有一段路要走。坚持以SOLID原则为指导来编写代码是极其重要的,要做到今后在不修改当前代码的前提下进行扩展,就得在编写代码时仔细考虑可扩展性。