前言
本单元作业主要以设计电梯来实现多线程编程。本章主要学习了如何使用多线程以及如何确保多线程安全,从电梯的调度策略中学会了如何简单地使用synchronized锁来控制线程安全。
首先,明确锁的两个常用的用法:synchronized修饰一个方法,synchronized修饰一个类。前者作用范围是整个方法,作用的对象是调用这个方法的对象。后者作用于关键词后大括号圈定的代码段,作用对象是这个类的所有对象。之后,便是自己代码的分析。
一、程序结构分析,
第一次作业
1)设计思路
第一次电梯作业实现的是一个简单的可携带的电梯,此时的电梯没有人数限制,并且只有一部电梯,对于要求范围的目的地都可达。第一次作业,具体难度在于,如何理解电梯的调度策略,并确保线程安全。第一次作业要预留足够的迭代空间,就得要考虑一些比较通用的问题。
首先,采用什么设计模式。因为第一周我们就学习了生成者-消费者模式,所以笔者就采用了这一种模式。这里并没有采用指导书的建议,将主线程设计为生成者线程,因为考虑到主线程的功能应该是简单地线程启动,以后迭代时,还可能会添加新的进程,所以单独设计了一个生产者线程。这里调度器(托盘),不作为单独线程,而是设置成一个请求队列,并且带有put,delete等方法。电梯作为消费者,不断的去访问队列,如果完成了一次就将队列中相应请求删去。总体的思路便是这样的一种生成者-消费者模式。
其次,调度策略的选择。整个单元的调度策略,我都选择了最容易实现的ALS策略,并且在经过每一楼层时都判断是否可以上下人,运行方向主要是根据主请求来确定的。主请求的选择,采用了纯贪心的思想,即,在当前楼层下,检索队列,此时再细分为电梯有人:选取最早到达为主请求。电梯没人:选取最近可以接到的请求为主请求。这里又涉及到了,电梯有没有人的判断,所以电梯类加入一个成员变量,考虑到以后的迭代问题(限定电梯内的人数),我们这里加入了一个people变量表示电梯内的人数。所以整个电梯类成员如下:
private Dispatch dispatch;//绑定队列 private int currenfloor;//记录当前楼层 private PersonRequestPlus mainrequest;//主请求 private int people;//电梯内人数 private int dir;//方向 public Elevator(Dispatch dispatch) {//构造方法 this.dispatch = dispatch; currenfloor = 1; dir = 0;//0静止,1向上,2向下 people = 0; }
PersonRequestPlus是用原有类进行扩展得到的,用途是加入了其他需要表示乘客状态的变量,如in(是否进入电梯)等。
此外,对于本单元作业,还有一个重要的点就是如何结束进程。
对于生成者线程,当输入NULL的时候,就表示生成者线程的结束。但是对于消费者线程而言,不再有请求加入不代表电梯就要停下,此时应该要完成之前所有请求才可以停下。因为我们完成一个请求就会将其从队列中删除,所以当所有请求都完成时,调度器里的队列应该为空。据此,在调度器设置两个布尔标志,Dead和Empty。对应四种情况:
empty |
|||
True |
Flase |
||
dead |
Ture |
整个线程结束 |
线程继续,不再有新请求,电梯继续工作 |
False |
电梯悬停,等待请求 |
正常情况 |
在线程安全方面,我对应的只有一个生成者和一个消费者,对put和delecte加锁即可。同时,因为调度器和队列重合使用,所以在消费者中,对请求队列进行循环查找时,要对调度器加锁(避免遍历的时候加入或者去掉了某个元素,造成错误)。类图如下(省去具体成员):
2)基于度量分析
首先是代码长度:
因为本单元作业有标准的生产者消费者模板,所以本次作业有面向对象的样子。每一个类各司其职,每一个类的行数都不是很多(电梯类主要模拟了电梯的运动,所以较长)。实际上为了更好的迭代,电梯类可以接着拆分,一个ElevatorBase类表示电梯的基本运动,例如上下行,开关门等模拟等。还有一个类,专门实现电梯类的调度方式,这样下一次迭代时,原来的基本运动不用更改,只需更换策略即可。
总体来说,没有很高的复杂度,实现了高内聚低耦合。
依赖分析上来看,没有出现红块,说明整体结构还是比较合理的。
第二次作业
1)设计思路
第二次作业相对第一次作业而言,主要是多电梯的考虑、楼层数量的增加,以及电梯人数的限制。电梯人数由于上次预留了空间,所以这次只要限制一个people的最大值即可。楼层数量增多问题不是很大,主要是注意到不存在0楼这个概念(即-1上一层为1)。所以本次更多需要考虑的是多部电梯的协调。本次作业,笔者为了安全起见,并没更改调度器,即还是采取调度器为请求队列,调度器不对请求进行分配,而是电梯进程启动后,由电梯自己去进行抢夺。并且为了不混乱请求队列,每一个请求再添加一个标志,表示属于哪一个电梯,上电梯时标记请求,出电梯时根据遍历同一个请求队列,根据已有的标志识别是否属于本电梯进行剔除。这种方案在请求越多时,由于自行抢夺,最终电梯所在楼层各不相同,新请求加入时,离其最近的会优先抢到,所以总体效率不会太低。但是不得不承认,这种调度策略虽然简单,但是现实中不会这么实现的,电梯会多出许多空跑,浪费电。基于本次建构想到的一个改进思路就是,电梯自己先模拟抢的过程,最后总会有一个最快抢到的电梯,这个电梯动,其他电梯不动。
该次作业,多电梯的出现,并且是采用自行抢夺的问题,那么线程安全上,最大的问题就是多电梯对公共队列的访问以及改写,所以对Dispatch类(调度器)进行加锁处理。这样很大程度上解决了线程不安全的问题。类图如下:
2)基于度量分析
首先是代码的长度
代码长度上几乎不变,本次作业采用了比较简单的调度策略,所以在上一次作业的基础上改动并不是很多,例如调度器Dispatch就没有动过,Producer也几乎没有改变。
这里复杂度较高的是inOrOut方法,其他复杂度不高。该方法复杂的原因是本人的进出都是遍历队列查找的,在这个时候out操作就是删除队列,如果不是线程安全的容器,就会报错,所以本人每一次删除都退出循环重新遍历,即双重循环,通过设置一个flag来退出大循环。
依赖程度与上一致,无较大变化。低耦合高内聚实现良好。
第三次作业
1)设计思路
第三次作业,是在第二次作业基础上的迭代,主要添加了几个难点:动态添加电梯,电梯电梯停靠楼层限定,以及不同种类电梯的运行参数。动态添加电梯,对于第二次作业启动电梯自行抢占的方式来说,只需确保线程安全即可;运行参数的限定,只要在电梯类加入新参数最后再根据不同的类型,在构造函数中赋予不同的初始值;最后,重点在于电梯停靠楼层的限定,限定问题就涉及停靠楼层(添加一个是否能够停靠的函数)该限定就增加了另一个需要考虑的问题——换乘问题。
本次作业重点需要解决的问题就是换乘问题。根据第一次作业的设想,电梯类实际上功能为两部分,一部分是基础运动,一部分是调度策略。所以第三次作业的换乘问题,本身上只需要改动调度策略。这里笔者采用最简单换乘方法,一个用户上电梯,先判断是否要换乘(添加一个是否能够直达的函数,即from和to的楼层都可以停靠),需要换乘,则将请求拆分,这就需要一个换乘地tem,使得(from,tem)对当前电梯可以直达,(tem,to)对其他类型电梯可直达。于是就改为两个请求,其中tem的选择优先考虑from和to直接的楼层,之后再考虑离二者最近的位置向-3和20散开。
类图如下:
类图基本与前两次作业一致,只是添加了一些功能。
2)基于度量分析
首先是代码长度:
三次作业代码行数299->357->482。三次迭代,代码稳定上升。但是第三次作业,明显电梯类有点臃肿,因为模拟电梯和调度策略都放在一个电梯类的中来实现,也就是之前说的,电梯类可以拆分成两个类。其他部分只是为了完成目标进行的适当完善。
本次作业的复杂度,在电梯类中,因为大部分的功能实现都在其中实现,所以电梯类中的许多方法的复杂度出现了红块。所以第三次作业的耦合度较高,不是很好的编程。
在类之间的依赖度分析上来看,与之前的几乎一致,没有很大的变化。
三次作业基本上使用该时序来完成作业,第三次作业特殊的地方是Produce额外产生了电梯进程。
3)设计原则角度分析
二、作业出现的bug
第一次作业
公测阶段:在强测中未发现bug,但是提交过程中出现了线程不安全的情况,主要原因是在电梯类对请求队列遍历时,生产者又产生新的请求加入队列,所以每一次遍历都加上锁。
互测阶段:未发现bug。
第二次作业
公测阶段:在强测阶段未发现bug,提交过程中经历了cputle的情况。主要原因是采用了电梯抢夺的策略,对于多电梯运作造成cpu过于繁忙。这里采用了某大佬推荐的sleep(1)的方法,每次抢之前先休息一会儿,如果抢不到,这个时候就会告诉cpu,就不占用cpu资源。
互测阶段:未发现bug。
第三次作业
公测阶段:在强测阶段未发现bug,提交过程主要是换乘的时候,由于是类打表操作,所以出现了未剔除0楼的现象,使得出现了许多bug(WA,RTLE都出现过)。
互测阶段:未发现bug。
三、分析别人程序bug策略
第一次作业:互测hack到了1次。采用的策略就是随机生成,所有数据都是手动随机构造(即自己随便写的),因为线程安全的问题有时候不是一定出现的,所以随机策略,能中就中。不过中的那一刀用的采用了时间上的特殊性,因为我们在开关门时间内可以进人,所以设想一个电梯到达了一个楼层进行开关门,这个时候加入新请求,看看会不会出现问题,果不其然就中了一刀。
第二次作业:相安无事。主要采用的是同时加入大量数据进行测试来测量有没有超容量的问题。之后还是采用随机的策略,进行随机打击。
第三次作业:构造大量需要换乘的数据并且在同一时间加入,这是一批数据。其次改变策略,想看看是否会出现提前结束的问题,例如按照自己的换乘策略,一个请求要换乘,就得把一个策略拆分成两个,即产生新的并删掉旧的。所以,如果是先删后加并且不是原子操作,很可能在某一时间出现了请求队列为空,电梯死亡。
四、心得体会
本单元作业的结束意味着课程已经过了一半了。自己最大的心得就是自己能力不断提高。尤其第一次作业时,完全理不清楚多线程的运行机理,到现在可以自己分析处自己线程不安全的地方,这里是花费了很多夜晚(头发)的。
还有就是本单元作业,因为有了比较标准的模式供我们参考,所以整体来说,很有面向对象的那个味了,特别是作业的迭代过程,已经没有上单元那样的大规模重构了,整体框架延用三周(也不罔顾了我第一周花了好久的时间才想清楚的结构)。而且代码风格上,也尽量的达到了高内聚低耦合的程度,算是比较大的突破。
还有值得自己比较庆幸的是,自己由于第一次作业的架构思路简单明了,出现wait的情况有且只有队列为空但输入未结束时,而notifyall只出现在添加请求上,所以,第二三次作业迭代的时候并没有出现因为胡乱加锁而出现的死锁问题。
但是很遗憾的是,自己由于第一次作业拖得时间比较长,没有考虑更好的想法,以及第二次写多电梯的时候,没有考虑分配请求。使得二三次作业,电梯采用了抢夺请求的方式,这种方式虽然时间上不至于超时,但是也有一定的功能损耗,虽然减少了出现bug的可能性,但是自己在多线程这一块没有得到更深一步的强化训练。同时,这一电梯策略非常不符合实际,有点应付题目的感觉。
最后,希望下一单元的挑战自己也能笑着面对(活着就好)。