BUAA_OO 第二单元总结
写在前面
? 多线程(multi-threading)是指从软件或硬件上实现多个线程并发执行的技术。现代处理器普遍具有多核的特点,支持同一时间执行多个线程,使用多线程技术可以提高程序并发度,整体提高处理性能。因此掌握多线程程序设计技术是CS学习必不可少的一部分。
? 多线程程序设计包括线程协同控制、线程安全保证以及线程程序设计模式等。本文主要总结OO课程第二单元所学的有关多线程程序设计的知识,实践体会,以及技术注记,以备后用。
第五次作业
? 第五次作业实现了一个单部多线程傻瓜调度(FAFS)电梯,目的在于初步掌握JAVA语言有关多线程的语法特性,同时进行初步的可拓展的框架设计
一、类图
? 第一次作业主要是让我们初步熟悉JAVA的多线程相关语法,主要考察的知识点是简单的线程协同控制,对于其他诸如线程安全问题等没有太多要求,因此整体比较简单。
1.1 代码结构分析
? 由上述类图可以知道,整个电梯系统可以分为以下模块:
InputHandler
: 指令输入管理器。负责接收指令输入,并将指令解析成PersonRequest对象,传给调度器Scheduler
Scheduler
:调度器。负责接收输入管理器传入的请求,同时进行调度管理(在这次作业仅是将请求传给该单部电梯)Elevator
:电梯。负责接收调度器传入的请求,并通过一定的逻辑执行请求,具有电梯属性和行为特征(如开关门,上下楼等)OutputHandler
:信息输出器。负责信息输出。如开关门、上下楼、乘客进出等信息输出。
? 除此之外,还有与电梯系统相关的数据结构:
RequestQueue
:请求队列。负责存放请求,由ConcurrentLinkedQueue
实现,作为InputHandler
和Scheduler
、Scheduler
和Elevator
的共享对象存在,是线程安全类。ElevatorConfig
:电梯属性类。负责初始化电梯属性,具有拓展性。
1.2 优缺点分析
- 优点
- 框架简单明晰:
输入管理器->调度器->电梯
的大体框架较为简单易懂且易实现。 - 使用线程安全集合:使用了
ConcurrentLinkedQueue
线程安全队列,一定程度上简化了读-写实现,减轻了对线程不安全的考虑负担。 - 初步考虑了拓展:将电梯属性单独抽象为一个类,并尝试使用了单例模式进行获取,使电梯属性具有拓展性;在调度器中设置了一个HashMap作为
调度器->电梯
的映射,出于后续作业可能的多部电梯的考虑。
- 框架简单明晰:
- 缺点
- 类抽象可进一步细化:
Elevator
类的逻辑可以进一步简化,特别是在后续作业中电梯功能进一步增多时,这一点可以帮助降低Elevator
类的复杂度,降低bug出现的概率。 - 未能提前改善电梯调度算法:仅实现了傻瓜调度,效率不高;可以提前尝试
ALS
、C-SCAN
、C-LOOK
等电梯调度算法。 - 未使用
wait/notify
来避免无谓的轮询:仅使用了暴力轮询来完成功能,导致CPU负载较高(虽然本次作业并没有对此做出要求)。
- 类抽象可进一步细化:
1.3 设计原则检查
- Single Responsibility Principle:类/方法的功能行为单一,符合该原则。
- Open Close Principle:一定程度上考虑了可拓展性,但依照开闭原则应该进一步增加框架弹性。
- Liscov Substitution Principle:无子类无继承,满足里氏替换原则。
- Interface Segregation Principle:无接口设计,满足接口分离原则。
- Dependency Inversion Principle:对楼层建模不够细致,需要进一步改进一满足本原则。
二、代码度量
? (度量符号说明参见注记,下面对个别新增度量进行注释)
? LOC
:Lines of Code: 类/方法代码规模
? NAAC
:Number of Attributes: 类属性个数
? NOAC
:Number of Methods:类方法个数
? CONTROL
:Number of Control statements:控制分支个数
- 代码统计
- 类度量
- 类复杂度度量
- 方法复杂度度量
类整体复杂度较低,方法整体复杂度较低。
三、UML协作图
四、Bug分析
? 在中测和强测均得到满分,暂时未发现bug
? 补充说明:(自测过程中的发现)
- 线程安全问题:
在本次作业的实现过程中,由于使用了
ConcurrentLinkedQueue
线程安全队列,以及使用暴力轮询,因此并没有显式使用synchronized
或Reentranlock
等JAVA锁机制来保证线程安全。由于任务简单,因此没有出现bug,但事后来看还是应该进行显式使用锁机制,一是为了避免未知不定的线程不安全,二是在可读性和代码含义上显式使用锁机制能够起标识警示作用。 - 测试问题:
多线程程序设计的另一大难点就是调试。由于线程执行的不确定性以及程序调试断点的特殊性(断点的设置可能会影响线程的行为),因此多线程程序的测试比单线程程序更加繁琐。
五、反思
? 第一次作业比较友好,主要是起铺垫作用,整个实现过程也比较顺利,耗时较短。同时自己也初步尝试了多线程模式进行程序设计,但整体来看对线程安全以及CPU使用考虑不够。特别是在拓展方面,其实应该提前考虑电梯调度算法,减轻后续作业的负担。不仅如此,在测试方面可以做得更好,毕竟多线程的debug没有单线程那么简单易行,涉及到时序,使用脚本对拍是一个更好的方法。
第六次作业
? 第六次作业要求实现一个单部多线程可捎带调度(ALS)电梯,目的在于进一步拓展单部电梯的功能,加强面向对象设计与并发设计训练,重点关注代码逻辑复杂情况下线程并发控制与线程安全保护。
一、类图
? 第六次作业的基本框架沿袭了第五次作业,同时针对新增的捎带功能进行了一定的拓展。
1.1 代码结构分析
? 新增了以下类:
ElevatorController
:电梯控制类。封装了部分电梯执行逻辑,如返回下一楼层,返回运动方向,捎带检测等逻辑。
? 修改了以下类:
RequestPack
:用于下面的楼层建模,为捎带功能服务。主要包含两个队列,一个为进入队列,一个为离开队列,对应于某个楼层。即当前在某楼层有多少要进入电梯,有多少要离开电梯的人。Elevator
:对电梯的行为进行进一步细化与拓展;增加了与捎带策略有关的数据结构,具体如下:RequestQueue
:请求队列:与调度器共享,负责接收调度器传入的请求,需要保证线程安全。MainRequestQueue
:主请求队列:用于存放主请求,属于对象内部变量,不需要保证线程安全。具体作用是,当请求不能被当前主请求捎带时则先加入该队列暂存。packMap
:捎带映射Map:相当于楼层建模,key
为楼层号,value
为RequestPack
类。
? 总体来说继承了第五次作业的代码框架,同时进行了一定的拓展,重点在于捎带功能的实现。捎带主体逻辑可以拆分成相关数据结构和相关算法:
- 数据结构:
- 调度器-电梯共享请求队列:沿袭了第五次作业的共享队列,调度器行为与电梯行为在该队列上与第五次作业保持不变。
- 电梯楼层map:作为楼层建模服务于捎带逻辑。具体来说,对每一有请求(进入请求、离开请求)的楼层设立进入请求队列和离开请求队列。对于某一时刻可捎带的请求,将其放入对应楼层的进入队列;对于当前楼层的进入队列请求,将其放入请求对应楼层的离开队列;电梯每运行一层,则检查当前楼层队列是否有需要处理的请求。这样可简化捎带逻辑。
- 电梯主请求队列:用于存放主请求候选请求,即是将不能由当前主请求捎带的请求放入该主请求队列中,当主请求及其捎带请求全部执行完毕后,再从该主请求队列中挑选请求作为新的主请求。
- 调度算法:
- 电梯捎带请求查询:每改变一次楼层,则立即扫描一次共享请求队列,将可捎带的请求放入相应楼层map,将不可捎带的请求放入主请求队列中。
- 电梯主请求设置:当主请求及其捎带请求执行完毕后,从主请求队列中挑选(未优化则是直接取队头请求)一个请求作为新的主请求,然后扫描一次共享请求队列,将可捎带的请求放入相应楼层map,将不可捎带的请求放入主请求队列中。
1.2 优缺点分析
- 优点
- 利用了代码的可拓展性:继承了第五次作业的代码框架,将诸如
InputHandler
、OutputHandler
等与新增功能无关的模块重用,仅在Elevator
类中增加逻辑,整体改动较少。 - 使用wait/notify代替暴力轮询:本次作业限制了CPU运行时间,因此需要使用wait/notify来实现线程间通信,同时减轻了CPU负担。
- 利用了代码的可拓展性:继承了第五次作业的代码框架,将诸如
- 缺点
- 捎带逻辑实现略显复杂:判断是否为可行捎带请求的逻辑条件分支过多,导致debug难度增大,可读性变差。
- 未使用效率更高的调度算法:对于单电梯系统而言,捎带(ALS)算法显然不是平均意义下的较优算法,诸如
C-LOOK
,C-SCAN
等算法可以提高电梯运行效率。
1.3 设计原则检查
- Single Responsibility Principle:类/方法的功能行为单一,符合该原则。
- Open Close Principle:相比第一次作业增加了拓展性,较符合开闭原则。
- Liscov Substitution Principle:无子类无继承,满足里氏替换原则。
- Interface Segregation Principle:无接口设计,满足接口分离原则。
- Dependency Inversion Principle:对楼层进行建模,较符合本原则。
二、代码度量
? (度量符号说明参见注记,下面对个别新增度量进行注释)
? LOC
:Lines of Code: 类/方法代码规模
? NAAC
:Number of Attributes: 类属性个数
? NOAC
:Number of Methods:类方法个数
? CONTROL
:Number of Control statements:控制分支个数
- 代码统计
- 类度量
- 类复杂度度量
- 方法复杂度度量
部分类(
Elevator
等)复杂度较高,一部分原因是其组合的类较多,另一部分原因则是其逻辑较为复杂,需要进一步细化功能。
三、UML协作图
四、Bug分析
? 在强测中出现了bug,错误信息显示是存在乘客未送到目的楼层;经代码检查后发现是捎带逻辑中的一个变量名写成了另一个变量名,导致部分捎带未能存入捎带map后未能响应。
? 本次作业在强测上较为可惜,主要责任在于自己未能进行全面的测试,特别是基本的覆盖分析也没有仔细进行,导致出现bug,引以为戒。
五、反思
? 第六次作业在第五次作业的基础上增加了对电梯调度性能的要求,实际上在以下几方面对我们进行了考核:
- 代码拓展:毕竟增加的功能只与电梯的调度有关,对于一个合理设计的框架来说,低耦合、鲁棒性是必不可少的;面对新的要求,可拓展的代码可以很容易地适应并进行增删改,起到事半功倍的效果。
- 抽象建模:如何实现捎带算法是本次作业的基本要求,通过对捎带调度的分析可以很容易地想到对以楼层为单位进行调度,这也是面向功能的抽象与建模。
- 调度算法实现:电梯性能与调度算法密切相关,因此不同的调度算法带来的收益也不同。扫描类算法实际上比捎带算法性价比更高,在保证性能的同时实现也较为简单。
? 从以上角度看,本次作业较好地完成了前两点,在第三点上还需要进一步改进,因为本次作业出现的bug一部分原因也是在捎带算法的逻辑复杂上,除此之外,高效的调度算法实现起来反而可能比低效的算法简单,因此后续作业尽量在性能方面多投入些精力。
第七次作业
? 第七次作业要求实现多部多线程智能(SS)调度电梯,进一步考察面向对象多线程程序的设计能力,重点关注多线程协同、线程安全与面向对象抽象设计、拓展设计的结合考察。
一、类图
? 第七次作业在第六次作业的基础上进行了改动,目的是为了拓展以下功能:
- 换乘功能:本次作业由于不同电梯的服务区间不一致,因此乘客请求可能会出现单部电梯无法完成的情况,因此需要支持换乘功能,即请求可拆分。
- 最大承载量:本次作业新增了电梯属性--最大承载量,目的是更加逼近实际场景(毕竟电梯不可能无限捎带)
- 不同电梯服务楼层区间不一致:本次作业不同电梯服务楼层区间不一致,因此需要针对这一特点在电梯线程实例化时实现可拓展、可多态性。
多部电梯调度优化:当一个电梯系统中存在多部电梯时,整个系统的性能除了与单部电梯对自己的请求集合进行调度优化相关以外,更重要的是与多部电梯之间协同合作有关。
1.1 代码结构分析
? 针对上述需要拓展的功能分别介绍本次作业代码结构的拓展:
- 不同电梯服务楼层区间不一致:
- 新增了以下类:
ElevatorConfig
:电梯属性类,存放电梯的开关门、上下楼速度、以及最大承载量。ElevatorConfigBuilder
:电梯属性建造类,相当于属性工厂,客户需要哪种类型的电梯属性,只需要询问该工厂,其就会返回对应的电梯属性。上述两个类相当于工厂模式,在电梯初始化时只需要通过
ElevatorConfigBuilder
即可获得所需要的电梯属性。拓展性也较好,只需要在该类中增加需要新增的电梯属性即可使用。
- 新增了以下类:
- 最大承载量:
- 修改了部分逻辑:
只需要在电梯进人与放人过程维护该量,当该量大于最大承载量时禁止捎带即可。
- 修改了部分逻辑:
- 换乘功能:
- 新增了以下类:
RequestSp
:特殊请求类,该类继承自PersonRequest
类,增加了诸如换乘层,第一次电梯和第二次电梯等属性;为换乘功能服务。SchedulerController
:调度控制类。该类服务于调度器Scheduler
,内部包含解析请求的方法,作用是对于输入的每个请求,为其安排合适的电梯。
- 修改了部分逻辑:
- 对于请求:之前作业的请求单部电梯执行一次便可执行完毕,而本次作业由于换乘功能的存在,需要对请求进一步细化:即无需换乘请求、第一阶段请求、第二阶段请求。
- 对于执行:调度器在解析请求时即可分析出该请求是上述哪种请求,并做相应操作:
- 无需换乘请求:直接传给对应电梯执行即可
- 第一阶段请求:传给第一阶段电梯执行,第一阶段执行完毕再将第二阶段请求传给调度器
- 第二阶段请求:传给第二阶段电梯执行即可。
由上述逻辑可以知道,
RequestSp
类需要支持各种请求的变种,来匹配功能。
- 新增了以下类:
多电梯调度优化:- 新增了以下类:
ElevatorData
:电梯信息类,包含诸如电梯当前层,电梯目标起始、目的层,电梯方向等电梯属性,主要是服务于多电梯调度算法。但由于个人原因并未实现动态的调度优化。
- 新增了以下类:
1.2 优缺点分析
- 优点:
- 代码拓展:继续继承之前作业的框架,特别是单部电梯的逻辑基本可以移植,本次作业新增的要求重点放在调度器相关逻辑解决即可。
- 灵活使用继承与多态:换乘功能的实现通过继承
PersonRequest
类和多态运用较为简便地实现,体现了OOP的特性与妙用。
- 缺点:
- 未能实现进一步多电梯优化:由于个人原因未能实现进一步优化,特别是对多线程协同和线程安全方面考虑较少。
1.3 设计原则检查
- Single Responsibility Principle:类/方法的功能行为单一,符合该原则。
- Open Close Principle:通过拓展增加新的换乘功能,符合开闭原则。
- Liscov Substitution Principle:对
PersonRequest
类进行了继承,并在父类出现的地方使用了子类代替,满足里氏替换原则。 - Interface Segregation Principle:无接口设计,满足接口分离原则。
- Dependency Inversion Principle:抽象较为细致,符合DIP。
二、代码度量
? (度量符号说明参见注记,下面对个别新增度量进行注释)
? LOC
:Lines of Code: 类/方法代码规模
? NAAC
:Number of Attributes: 类属性个数
? NOAC
:Number of Methods:类方法个数
? CONTROL
:Number of Control statements:控制分支个数
- 代码统计
- 类度量
- 类复杂度度量
- 方法复杂度度量
可以看到复杂度仍然集中在
Elevator
和Scheduler
类中,代码逻辑仍然需要进一步细化。不仅如此,抽出的静态类方法ElevatorController
和SchedulerController
虽然在逻辑上简化的电梯类和调度器类,但在数据上似乎反而增加了复杂度,这与静态类的计算有关,但我认为这样的做法确实简化了逻辑复杂度。
三、UML协作图
四、Bug分析
? 在中测和强测均通过正确性,但是在性能方面没有获得较高收益。
五、反思
? 第七次作业是电梯系列作业的终点,在各方面均对OOP程序设计能力进行了较高的要求,这里引用助教的话详细说明:
- 基础部分,那么需要做的是设计好线程安全的架构,然后把工作流的先后顺序处理好
- 具体来说,工作流处理指的是指令的拆分,比如对于指令2-->3,如果拆为2--B->1和1--C->3的话,那么毫无疑问,1--C->3必须在2--B->1之后执行,才符合逻辑顺序。
- 本次基础部分的架构设计所需要处理的问题就是这些- 进一步要求,那么则需要继续像上次一样,使用各类优化算法(SCAN、LOOK、ALS等),并且将这些优化算法进行最大限度的逻辑抽象和逻辑封装,做成统一建模、可配置、零耦合、可独立运行的三条电梯线程,并行工作。
- 极限性能,则各自完全独立的调度算法就不一定好使了。正如楼上部分同学所说,需要的是多电梯协同配合。就好比打游戏团战时,三个人默契的配合效果必然强于各自为战。这样一来,对调度器架构的设计会提出更高的要求——需要能进行跨线程之间的协同,并基于不同线程的实时状况进行优化。或者说,所设计的架构必须要能充分支持跨线程的实时优化,你才有足够的潜力在不翻车的情况下进行极限优化。当然,除此以外,则需要的是一些脑洞了,甚至完全不必拘泥于传统的调度算法,可以自由发挥想象。
? 总而言之,架构设计是基础,单体算法与低耦合是进阶要求,多模块协同与交互是较高要求,三者相辅相成,在后续作业中需要进一步加深理解与强化练习。
技术总结
一、多线程设计
- 线程协同
- 暴力轮询
- wait/notify
- Producer-Consumer模式
- Worker-Thread模式
- 线程安全
- Java线程锁机制
- Java线程安全集合
二、测试技术初步
- 黑盒测试
- 生成数据:此处强调自动对拍即生成数据检测在多线程调试过程的重要性。因为多线程程序的不确定性,白盒测试对代码静态逻辑检查已经不能保证万无一失了,随着逻辑复杂度的增加,代码静查的困难度也剧增,因此黑盒测试才是较为保险的测试手段。
- 覆盖测试
三、设计原则思考
- 设计原则--总原则:开闭原则
开闭原则就是说对扩展开放,对修改关闭。在程序需要进行拓展的时候,不能去修改原有的代码,而是要扩展原有代码。即是为了使程序的扩展性好,易于维护和升级。
- 单一职责原则
- 里氏替换原则
- 依赖倒转原则
- 接口隔离原则
- 迪米特原则(最少知道原则)
- 合成复用原则
注记
一、代码度量工具说明
- 使用工具
- IDEA 插件:Metric Reloaded
- 使用:安装完成后,Help --> Find Action --> Calculate Metrics
- 两类分析工具说明
- Chidamber-Kemerer metrics
- CBO: 类间耦合度
- DIT: 继承树深度
- LCOM: 类内聚度
- RFC:类响应度
- Complexity metrics
- Class metrics
- OCavg : 类方法的平均循环复杂度
- WMC:类方法权重(可理解为方法数量等)
- Method metrics
- ev(G): 基本复杂度
- iv(G): 模块设计复杂度
- v(G): 模块判定结构复杂度
- Class metrics
- 其他度量:
LOC
:Lines of Code: 类/方法代码规模NAAC
:Number of Attributes: 类属性个NOAC
:Number of Methods:类方法个数CONTROL
:Number of Control statements:控制分支个数
- Chidamber-Kemerer metrics
二、设计模式学习
- 《图解Java多线程设计模式》
原文地址:https://www.cnblogs.com/lsj2408/p/10744063.html