OO学习体会与阶段总结(多线程程序)
前言
在最近一个月的面向对象编程学习中,我们进入了编写多线程程序的阶段。线程的创建、调度和信息传递,共享对象的处理,线程安全类的编写,各种有关于线程的操作在一定程度上增加了近三次作业的复杂度与难度,带来了不小的考验。本文通过分析总结近三次作业的完成情况,分享我对与多线程编程的一些见解与体会。
作业总结分析
多线程电梯调度
(1)题目简述
实现具有捎带功能的电梯调度系统,调度电梯数量为3部。
(2)程序设计
- 本系统的大致结构与之前的单线程电梯调度系统类似,主要由输入处理、请求调度、电梯模拟三大部分组成。
- 根据行为的并发特征,为这三大部分各设计了一类线程来实现其功能。分别为输入监听线程、请求调度线程、电梯线程。
- 系统中的共享资源主要是请求队列,根据请求队列的类型分成了输入队列与执行队列。二者继承了请求集合类,并将这两个类编写成线程安全类。
- 根据生产者-消费者模型构造线程交互方法。
- 具体类图如下:
-
时序图:
(3)程序分析
- 复杂度度量:
-
分析:
- 在存储请求队列时采用了数组的数据结构。进行请求队列的增减操作时需要遍历数组,导致了相关方法循环复杂度较高。由于运用了长度变量控制遍历的边界,这些方法不会出现危险。可以考虑使用集合类管理此类数据,以减小代码的复杂性,增加可维护性。
- 线程类的run方法过于复杂,使得该方法的复杂度异常的高。出现该问题主要由于对于线程类的功能的理解不够到位。应当将线程的行为细化,并根据线程运行时需要执行的不同操作编写相应的方法以供线程调用。这样能提高线程类的可维护性。
- 数据统计:
(4)问题分析
本次作业的问题主要出现在以下两个方面:
- 捎带请求处理:本次作业没有采用前几次作业处理捎带请求的方法,而是根据电梯的运行将分配到的请求按照到达先后顺序构造队列,电梯按照队列头改变运行状态即可。但是在将新请求插入队列的部分,边界判断出现了错误,导致当电梯停止时新请求的插入位置出错。最终使电梯运行路线出错。
- 输出格式:对于不合法的输入,输出时的格式不对。更改输出格式可以改正。
本次作业使第一次编写多线程程序。由于对于每个线程的运行过程的理解不足,我在最初的程序构架时遇到了比较大的困难。尤其是在共享类中锁的运用以及线程类的run方法的功能的设计上,经历了多次删除重写的过程。在一定程度上,这导致了我在类的设计、调度算法设计等方面有了不少疏忽。最终使得本次作业的完成效果一般。此外,在线程调度方面,本次作业较好的运用了课内讲的模型。通过这次的锻炼,我对于多线程编程的大体框架有了比较深刻的认识,为接下来的学习打下了铺垫。
IFTTT
(1)题目简述
实现IF-THIS-THEN-THAT的文件监控与操作系统。
(2)程序设计
-
经过分析,程序大致分为文件快照获取、监控事件触发、监控事件任务执行三部分。
- 根据行为的并发特征,为每个监控事件开启一个线程,实现监控功能;并且设计一个负责实时输出的线程。
-
具体类图如下:
-
时序图:
(3)程序分析
-
数据统计:
-
复杂度分析:
-
分析:
-
在构造文件系统的遍历树是采用了一维的线性数据结构,每次获取快照时都需要按深度递归遍历监控范围内的所有文件,导致获取快照的相关方法的复杂度较高。在优化的方面,可以考虑改用集合类中的树结构或者哈希表的方式对监控范围内的文件进行预处理(如编码),减小之后的工作量。此外还可以直接使用现有库中的获取快照的方法。
- 本次作业对与线程类中的run方法进行了一定的精简。相较于上次作业,复杂度有了一定的降低,但仍然是所有方法中最高的。可能的原因是我在最初设计时为单一线程分配了过多的功能。就本次作业而言,可以按照触发器的不同将监控线程进行分化。而且还可以将快照获取功能分离成单一线程,通过一个线程安全类与监控线程传递信息。
-
(4)问题分析
本次作业的问题主要出现在文件地址的管理方面。在程序中,每个文件都使用的是绝对地址格式。绝对地址有多种可识别格式,但通过File类提供的方法从File对象中获取的绝对地址仅有一种格式。由于在前后快照对比是通过字符串比较实现的,所以可能由于格式问题导致错误。将格式同一后可以解决。
在本次作业中最重要的部分就是文件信息的同步。由于文件操作是线程不安全的,所以在完成作业的过程中,我用了相当多的时间在设计线程安全类和快照获取功能上,最终获得了不错的效果。但在类功能的设计上还显得有些冗杂,有挺大的改进空间。
模拟出租车打车系统
(1)题目简述
实现100辆出租车的抢单调度系统。
(2)程序设计
-
- 乘客交互的时间特征:不定时产生乘客请求;请求产生3s后返回处理结果。
- 出租车交互数据特征:获取出租车的位置信息、服务状态和信用信息;返回其抢单结果和需要服务的乘客位置信息与目的地位置。
- 出租车交互时间特征:以100ms为周期获取。
-
-
-
乘客的请求:
- 监听获取乘客发出的请求。
- 维护接受的请求集合。
-
乘客请求的响应窗口:
- 根据请求的位置与时间建立相应的抢单窗口。
- 维护相应窗口集合。
-
出租车:
-
维护出租车信息的更新。
-
维护出租车集合。
-
-
-
-
-
系统与乘客之间交互相对独立,由于只由控制台输入,使用一个线程设计用于与控制台输入交互。
-
出租车的行为具有显著的重复模式,并且相对独立,使用一种线程设计,分别描述每一辆出租车行为。
-
对于获取的乘客请求的处理与分配具有重复性,并且相对独立,使用一种线程设计来实现功能。
-
信息输出相对独立,使用一个线程实现。
-
-
具体类图如下:
-
时序图:
(3)程序分析
-
数据统计:
-
复杂度分析:
-
分析:
- 相较于上次作业,本次作业方法的复杂度有了明显的降低(不考虑现成的GUI代码)。在最初设计的时候,我就考虑到了SOLID等原则,并尽量使设计的类符合以上的原则,做出了风格上的改变。其中线程类的设计上,尽量分配更少、更简洁的功能,使其仅需完成线程部分必要的代码。从而使得整个程序的可维护性与可读性有了相当大的提高。
(4)问题分析
在测试阶段我的程序没有被发现问题。但是在自己检查的时候发现了不少的潜在问题:
- 地图管理:本次作业我使用了gui中给好的地图管理类,使用矩阵管理地图。每当出租车接单时都需要调用广度遍历方法来获取最短路径。这使得出租车在路径的选择上缺乏功能延展性,只能走最初设定好的路线,无法适应路况的变化。
-
出租车管理:在本次作业中,我为了简单将所有的出租车都管理在了同一个数组中,将出租车状态保存在了出租车各自的对象中。如果能将不同状态的出租车分离,用不同的策略去管理,则能够使程序的延展性更高。
总结
经过了这三次的多线程编程作业,通过分析作业中的问题,我对于线程的调度与线程的安全有了相当深入的认识。从宏观上来看,每个线程都在同时运行,但微观上看他们是在JVM的调度之下按原子操作交替执行。如果每个线程间相互独立,JVM调度的不确定性不会影响程序的可再现性。但就像电梯中的请求队列、IFTTT中的文件信息、出租车系统的请求和出租车信息,每个线程间难以避免地会有共享的数据,此时就需要通过同步、互斥的手段防止JVM调度的不确定性破坏程序的可再现性。通常,通过锁机制就能够实现这些功能。但是,在共享关系比较复杂的情况下,单纯的使用锁机制并不一定能够达到预期的效果。这时就需要一种模式化的线程安全保护措施。那就是线程安全类。编写线程安全类就好比为已有的功能代码加上一层外皮,其内部代码保证功能的实现,外部接口保证入口的互斥,从而实现线程安全。但这又带来了另一个问题:同步部分的代码长度对多线程效率的影响。进而对于临界区的功能安排应当尽量精简,以避免对多线程机制的浪费。
此外,经过了对面向对象思想的学习,我认识到了面向对象程序设计的12大基本原则,并且将其实践到最近的一次作业当中。在亲自编写代码的过程中,我体会到这些原则最核心的想法就是:设计具有层次性、代码具有可延展性。在设计时就要从最外层的交互设计,一层层深入,到内部对象的建模、对象间交互,再到类内部的设计。这个设计就是对整个环境与系统的逐层深入,将各个功能逐层分离,最终形成类似树状的类设计。而在编写代码的时候不应当仅关注于当前的功能实现,更应当想到更多同类型的操作。换句话说就是编写出的方法、类不应当进能够实现特例操作,而更应当面向更为抽象、更为通用的层次上。
下一篇: 象棋实战:基于QT5.7