OO第二单元作业总结——多线程
单元任务
本单元主要的内容是通过模拟电梯的运行来熟悉多线程的实现,从简单的单部FAFS电梯开始,ALS电梯,到最后的多部ALS电梯。
一、设计策略分析总结
1.1 多线程协同
这三次的作业都需要通过多线程来实现输入和电梯执行的并发。因为吸取了第一单元的教训,在最开始设计的时候,就尽量考虑到之后的可扩展性,尽量使每个部分各司其职,相互之间减少关联性。
第五次作业
第五次作业主要分为四个模块:主线程、输入线程、调度器即请求队列、电梯线程。
主线程主要是控制整体程序执行的。
输入线程的编写参照了课程组给出的demo,主要的功能是从标准输入中读取请求,并将请求添加到请求队列中。
因为是FAFS电梯,所以这次作业的调度器没有什么算法设计,电梯线程每次取请求的时候,将队列中的第一个请求返回即可,因此调度器只需要维护一个请求的动态数组,方法包括添加请求和取出请求。
电梯线程每次只取一个请求,在当前请求执行完毕之后,才会向调度器取下一个请求。
第六次作业
本次作业的电梯为ALS电梯,即可捎带电梯,依旧是分为四个模块:主线程、输入线程、调度器、电梯线程。
与第五次作业的主要差别是调度器需要提供查询可捎带请求的功能,根据电梯当前的楼层数以及运行状态,来搜索请求队列中所有可能被捎带的请求,并将这些请求返回给电梯线程。具体的算法是:如果电梯处于上行状态,将请求队列中所有起始楼层与电梯当前楼层相等并且上行的请求添加到一个新的队列中,将这个队列返回,同时移除返回的请求;如果电梯处于下行状态,将请求队列中所有起始楼层与电梯当前楼层相等并且下行的请求添加到一个新的队列中,将这个队列返回,同时移除返回的请求。
因为考虑到性能优化,当电梯需要改变运行方向的时候,调度器为电梯线程提供了查找当前楼层之上的请求和当前楼层之下的请求的方法。这两个方法不会考虑请求的方向与电梯当前运动方向是否相符,而是根据起始楼层与当前楼层的关系,返回第一个满足条件的请求同时将其从队列中移除。
电梯线程需要维护当前楼层、运行状态、请求队列。当电梯中的队列为空时,向调度器取一个主请求,这个请求是还未执行的请求中到达时间最早的请求。电梯以主请求为主开始运行,因为此时主请求进入了队列但乘客还没有进入电梯,所以我还给电梯设置了一个Boolean型变量firstIn用于标记主请求是否已经进入电梯。当电梯中的队列不为空时,电梯开始运行直至队列再次为空。
每次运行时,首先根据firstIn判断主请求是否已经进入,如果没有,则从当前层数运行到主请求的起始层数,如果进入了,则从当前层数运行到主请求的目的层数。运行过程中,每到达一层,检测队列中有没有可以出去的乘客,再向调度器获取可以捎带的请求,加入队列。
每当电梯可能出现改变运行方向的时候,现在自己的队列中查找是否还有同方向的请求还未执行,如果有,将其设为主请求,即将该请求移至队列首部,继续执行;如果没有,向调度器查看运行方向上是否还有请求,如果有,以该请求为主请求继续执行,同时firstIn设为未进入,如果没有则改变方向继续执行队列中的请求。
第七次作业
本次作业是同时调度三部电梯,同时每部电梯的停靠层数、限乘人数、运行速度均不相同。有一些请求是不能直接送达的,需要进行拆分,而拆分之后的请求是存在时间上的依赖关系的,例如,第二个请求必须在第一个请求执行完毕之后才可以写入请求队列中,不然可能会出现一个乘客同时出现在两部电梯中。为了处理拆分的请求,我创建了一个类TransRe对所有的请求进行包装。TransRe中只包含一个队列,用于存放拆分之后的多个请求。请求如果没有拆分,则对应的TransRe的队列中只有这一个请求,即长度为1;请求如果拆分成多个,则对应TransRe队列中会有多个请求,请求之间存在严格的先后关系。由此来判断出一个请求是否执行完毕。
主线程和输入线程的功能和前两次作业相同。
因为此次的调度器功能十分繁复,我将其分为了两层:上层调度器和下层调度器。
上层调度器的任务是将输入线程读到的请求根据不同电梯的停靠层数进行拆分,分配给各个电梯的下层调度器,同时通过集合维护还没执行完的请求的用户id。上层调度器的属性中包括所有与其对应的下层调度器,可以获得每部电梯的可停靠楼层,如果一个请求可以由一部电梯独立完成,则将该请求分配给该电梯调度器;如果不可以,则分别选取起始楼层可停靠电梯和目的楼层可停靠电梯,求两部电梯可停靠楼层的交集,由此划分请求,分配给起始楼层的电梯,将用户id加入集合中。如果新来的请求用户id在集合中,就将其从集合中移除,说明不再是划分请求。每个请求都会经过TransRe的包装。
下层调度器与第六次作业中的调度器功能相似,只不过变量类型由PersonRequest变为了TransRe,多维护了一个电梯可停靠楼层的集合,在查询可捎带请求时多出了数量要求。每个下层调度器都只为一部电梯服务。
电梯线程与第六次作业相似,多出了电梯编号、限乘人数、每层运行时间的属性,每次查询可捎带请求、向上或向下查询请求时,都需要判断电梯是否已满。每执行完一个请求,还要判断是否是划分请求,如果是,移除TransRe中的第一个请求,再写回上层调度器。
这三次作业中,我认为电梯线程的结束条件是一个十分重要的逻辑判断点。输入线程的判断条件课程组已经通过demo告诉我们了,而电梯线程何时结束关系到程序是否能正确运行。第五次和第六次的作业中,我的判断条件都是in.isAlive() || list.getLength() != 0,即输入线程没有结束或者请求队列不为空,意味着还有请求没有执行。但到第七次作业中,多部电梯时如果还使用这个判断条件会使程序进入忙等状态,当队列中剩余一个请求时,一部电梯取走了这个请求,其余两部电梯进入等待状态,可这时没有其他线程可以唤醒这两个线程,因此程序进入忙等状态,不会停止。
因为这个原因,我改变了判断条件,如果输入线程结束,并且上层调度器的未执行完请求集合为空,则向每个下层调度器中写入new PersonRequest(0, 0, 0)请求,这个请求不会被任何捎带查询返回,只会被获取主请求方法返回,因此电梯只要执行到该请求,说明请求已经全部执行完毕,可以结束线程了。
1.2 同步控制
对于多线程程序而言,最重要的是共享数据的同步控制。在这三次作业中,共享数据都是请求队列,第五次和第六次的作业都是一个总的请求队列,第七次作业的共享数据其实是三个下层调度器的请求队列。
因为输入线程的写操作和电梯线程的取操作都会对共享数据造成修改,所以这两个方法都需要加上synchronized进行修饰,即将共享数据锁住,使取和写不会同时进行,以此来保证共享数据是线程安全的。
二、基于度量分析程序结构
第五次作业
2.1.1 代码规模
2.1.2 类图
2.1.3 类和方法复杂度
2.1.4 UML时序图
2.1.5 分析
可以看出,第五次作业还比较简单的,代码行数不是很多,每个类,每个方法的复杂度也不是很高,每个类相对独立,类中的属性也不是很多。
2.1.6 SOLID原则
[S] Single Responsibility Principle (单一功能原则):有较好实现,每个类和方法都有单一的功能。
[O] Open Close Principle(开闭原则):有较好实现,因为考虑到之后的可捎带和多电梯,模块划分的比较合理,扩展功能只需要添加方法即可。
[L] Liskov Substitution Principle(里氏替换原则):因为没有使用继承,所以不予讨论。
[I] Interface Segregation Principle(接口隔离原则):因为没有使用接口,所以不予讨论。
[D] Dependency Inversion Principle(依赖反转原则):有较好实现,三个主要的类内部的执行与另外两个类的内部执行没有依赖关系,依赖的是抽象的方法。
第六次作业
2.2.1 代码规模
2.2.2 类图
2.2.3 类和方法复杂度
2.2.4 UML时序图
2.2.5 分析
可以看出,第六次作业与第五次作业在类的数量和类之间的关系上没有发生变化,但因为要实现可捎带功能,电梯线程和调度器的代码量有了明显的增长,电梯类过于复杂,导致OCavg和WMC过高,其中的很多方法也出现了复杂度过高的情况,这是因为我的调度基本上都是电梯来完成的,请求队列的调度只是提供了一些获取相关请求的方法,内部的执行顺序还是靠电梯自己来调整。其次还是因为我的功能划分不够细致,如果将每个功能都写一个方法,方法的复杂度应该会有明显的减少。
2.2.6 SOLID原则
[S] Single Responsibility Principle (单一功能原则):没有实现,电梯类过于复杂,其中的许多方法的都具有多个功能。
[O] Open Close Principle(开闭原则):有较好实现,因为考虑到之后多电梯,模块划分的比较合理,扩展为多电梯只需要创建出新的电梯线程对象。
[L] Liskov Substitution Principle(里氏替换原则):因为没有使用继承,所以不予讨论。
[I] Interface Segregation Principle(接口隔离原则):因为没有使用接口,所以不予讨论。
[D] Dependency Inversion Principle(依赖反转原则):有较好实现,三个主要的类内部的执行与另外两个类的内部执行没有依赖关系,依赖的是抽象的方法。
第七次作业
2.3.1 代码规模
2.3.2 类图
2.3.3 类和方法复杂度
2.3.4 UML时序图
2.3.5 分析
这次作业虽然是多电梯,但其实在类的设计上与但电梯没有太大的差别,增加了一个调度类的原因是想避免单个调度器出现过于复杂的情况,然而上层调度器的算法依旧过于复杂。同时电梯类因为与第六次作业使用的几乎是相同的代码,复杂度依旧很高。增加了一个TransRe类用来包装拆分的请求。除了这些之外与第六次作业没有太大的改动。
2.3.6 SOLID原则
[S] Single Responsibility Principle (单一功能原则):没有实现,电梯类过于复杂,其中的许多方法的都具有多个功能。
[O] Open Close Principle(开闭原则):有较好实现,只需要改动主线程,就可以创建不同情况的电梯对象。
[L] Liskov Substitution Principle(里氏替换原则):因为没有使用继承,所以不予讨论。
[I] Interface Segregation Principle(接口隔离原则):因为没有使用接口,所以不予讨论。
[D] Dependency Inversion Principle(依赖反转原则):有较好实现,几个类内部的执行与另外的类的内部执行没有依赖关系,依赖的是抽象的方法。
三、分析自己程序的bug
- 第五次程序在强测中没有出现bug,因为没有性能要求,所以是满分。
- 第六次程序在强测中也没有出现bug,但是扣了一些性能分,看了强测的输出结果和自己的代码,我发现我的电梯在最后一个人出电梯之后没有办法把不同方向的请求捎带上,例如,当电梯下行至某一层,最后一位乘客出电梯了之后,如果这一层上有上行请求,电梯需要先关上门,再开门让这个上行请求的乘客上电梯,这样就多出了一次开关门的时间。这个bug我在第七次作业的时候修复了。主要原因还是因为电梯状态划分的不够明确,其实应该分为上行、下行和停止三个状态,一旦电梯为空,将状态设为停止,这样在查找捎带请求时就不会出现遗漏的情况。
- 第七次作业的bug都是出在运行时间过长上面,都是电梯线程进入等待状态,从程序的运行上来看,应该是标志终止的全0请求没有被写入低层调度器,即应该是在换乘请求执行完之后没有请求再次写入,导致结束标志不能写入低层调度器,导致所有的电梯都处于等待状态。
四、心得体会
首先第一次接触多线程感觉还是很有意思的,虽然一开始不是很熟练,但熟悉了之后,一个一个线程思考其实和单线程差不多,主要会出现的问题就是共享数据和线程停止条件。
线程停止条件在第七次作业中可是让我吃了苦头,前两次的判断条件都因为换乘的请求不能使用,只好另辟蹊径,但之后又带来的一连串的问题,多线程的程序还不能进行断点的debug,只好自己思考可能是哪里出现了死锁。
再有一点,在这个单元中,我明显感觉到初期设计框架的重要性,因为考虑到后面可能出现的多电梯,所以我一开始的结构划分就比较明确,第六次和第七次作业都是在第五次的架构的基础上进行修改和添加,整体结构没有太大的变化,不需要进行重构真的是节省了很多时间。