一、三次作业的设计策略
三次电梯模型其实都是类似的,都是生产者消费者模型,在电梯中托盘其实就是调度器中维持的一个请求队列,模拟输入不停的往调度器里面放请求,然后调度器再将请求分给电梯进行处理。所以三次作业我的总的大框架并没有发生很大的变化,主要都是:(1)构造一个总调度器,其中维持一个大的请求队列;(2)主线程完成从标准输入读入请求的工作,并将请求都放入总调度器的请求队列中;(3)总调度器不断的将请求分配给电梯,电梯处理自己得到的请求。
(一)第一次电梯作业
这次电梯作业中只有一个电梯运行,所以我的程序结构就很简单,就是分为上述的三个部分,即总调度器,主线程即输入线程和电梯,完成的工作就是:输入不停的往共享的请求队列中添加请求,电梯每次从队列中取出一个请求去完成,完成一个请求后再从队列中取出下一个请求继续执行。
- 关于线程如何停下这一点,我是通过在调度器中设置了一个布尔类型的标志 cancelled 。主线程不停的读入请求,当读入到null时,表示输入结束了,于是会将调度器中的这个标志置为True ,电梯每次从调度器的请求队列中读取请求时,都会判断这个标志位,当发现变为True 且请求队列已经为空时,我会返回给电梯一个null,当电梯取请求得到null时,则不会再继续循环执行请求,而直接结束了。这样完成了电梯和主线程正常结束的功能。
(二)第二次电梯作业:ALS电梯
这次的电梯作业仍然只有一个电梯,也就是说总调度器收到的请求仍是只给一个电梯,所以总调度器的分配策略并未发生变化。而本次作业新增加了捎带的调度要求,也就是说在电梯运行过程中,还需要在每一层楼判断是否有满足条件的人可以进来,即请求需要和楼层相关联,于是在第二次作业中调度器中的请求队列采用了新的存储形式。同时电梯内部也增加了存内部人员的队列。
- 调度器中维护了一个对应19层楼的list,同时每层楼 i 中还包含了上行up和下行down两个链表,用于存储在第 i 层楼等待的要往上走或往下走的请求。
- 电梯内部也维护了一个对应19层楼的list,每层楼 i 中也包含了上行up和下行down两个链表,用于存储此时在电梯内部且要在第 i 层楼出电梯的人。(显然电梯内部的list并不需要up和down这两个子链表,但因为我都已经定义好了这个类用于调度器的队列,所以我就直接偷懒拿过来放在电梯内了)
- 电梯内部维护的属性也从只有电梯当前层 floor 新增了 state 和 destination 两个属性,分别用来表示电梯此刻运行方向即上行还是下行,以及电梯本次上行或下行将会达到的最终楼层。
- 电梯内部的调度策略:在本次的调度中,我采用了每次电梯从主调度器中取一个达到时间最早的请求,然后根据电梯当前层floor 与该请求的 from 、to的关系决定电梯的运行方向。比如一直向下走,或先下后上、先上后下(对于这种需要折返的,我将电梯运行拆成了两部分,分为两个方向的单方向运行)等。同时将每次单方向运行会到达的最远的地方赋值给 destination ,电梯在单方向运行的过程中,每到一层楼判断是否有要出电梯的人,是否有在该楼层等待且请求运行方向和此时电梯state 相同的人,若有,则从总调度器中取出这个请求,进电梯,同时若该请求要去的楼层比当前的destination 更远,则将destination 更新为这个请求的to。每层楼处理完in、out的请求后,判断当前楼层floor 和目标楼层 destination 是否相等,若不相等,则电梯沿着此时方向继续运行;若相等,则结束本次单向移动,若还有下一个单向移动(针对之前取下来的主请求有折返的),则如法炮制。否则此时电梯中一定已经是空的了,则继续从总调度器中再取出一个请求,完成类似的操作。
- 程序结束方法:这次作业结束的方法采用了和第一次作业一样的方法。
(三)第三次电梯作业:多电梯
对比上一次作业,这次作业中出现了三个电梯,且每个电梯可停靠的楼层、运行时间都不一样。这样的电梯对于某些请求而言并不能通过单部电梯完成,但根据研究每个电梯的特点可以发现,每个请求最多能被两个电梯完成,也就是说只需要换乘一次即可。在第三次作业中,每个电梯都会去拿请求,而总的请求需要一个管理队列,所以我构造了两层调度的结构:输入线程不断的往主调度器中放请求,每台电梯配置一个子调度器,主调度器通过通过分配算法将请求分别分给不同的子调度器,每个电梯根据子调度器调度执行请求。所以,本次作业的整体架构为:一个主调度器里面挂三个电梯和三个子调度器,主线程不停的将请求放入总调度器中,主调度器根据各个电梯的特点以及当前的状态将请求分配给不同的子调度器,电梯在各自的子调度器的调度下执行请求。架构还是很清晰的,本次作业的重点还是在于总调度器的分配策略以及电梯的调度策略,以及同步问题。
- 本次作业中的子调度器相当于前两次作业中的主调度器,所以本次作业中三个子调度器的存储请求的队列结构和第二次作业相同,同时每个电梯内部的存储电梯内请求的队列也与第二次作业相同
- 电梯新增了最大载客量 maxIn 以及电梯的运行时间 runTime
- 为了解决换乘的问题,我定义了一个新的类用来存储请求
- 主调度器的分配策略:我的分配策略是,能直达的最好直达,不能直达的再进行拆分。先从主调度器中取下一个请求
- 电梯的策略:这部分的改动并不大,我只是在第二次的作业的基础上,进行了略微的改动。
- 在第二次作业中,电梯每次单向运行到了 destination 层就完成本次运行了,但考虑到,比如电梯此时是在上行,可能在比destination 层更远的地方,还有请求要上行。此时这种请求是应该需要被直接处理的。因此,在电梯每次到了destination层的时候,加上一个判断该方向上更远的地方是否还有同向运动的请求,若有,则电梯应去接这个请求。此外,我还新增了,电梯在 destination 层结束时,判断该层楼是否有要与当前电梯运行方向相反的请求,若有,则进电梯,反转电梯的运行方向,继续运行。
- 在每层楼的判断阶段,对于能出电梯的人,需要判断这是否是一个换乘请求,若是,则需要生成换乘的下半段并放入主调度器中;对于能进电梯的人,需要判断此时i电梯是否已经满了,若满了则不能让这个人进来
- 因为多个线程均要输出,为了保证输出的线程安全,我单独开了一个类,并使用上锁的方法进行输出。
- 程序结束方法:与前两次作业有所不同,主调度器接受到的请求不仅有主线程,各个电梯也可能会生成新的换乘请求给主调度器。所以主调度器的停止条件变为了主线程的输入结束,以及主调度器中的队列为空,且不可能再有换乘请求进入时,主调度器结束,同时通过置三个电梯的 cancelled 值,告诉三个电梯输入结束了。从而完成了所有线程的结束。
二、基于度量的程序结构分析
- DesigniteJava 分析结果的各项含义分别为:
(一)第一次电梯作业
1、类图
2、UML协作图
3、度量分析
(1)methodMetrics
(2)typeMetrics
4、分析
这次作业结构简单,各个类的功能独立
(二)第二次电梯作业
1、类图
2、UML协作图
本次作业中仍然是一个电梯,整体架构较上次作业并无大变化,所以协作图相同
3、度量分析
(1)methodMetrics
(2)typeMetrics
4、分析
本次作业中总共有五个类,其中电梯类的方法最多,其中方法的复杂度较其他类也更高。电梯中的各个方法写的不够简洁,很多功能比较混杂。但别的类中各方法的长度都还比较适中。
(三)第三次电梯作业
1、类图
2、UML协作图
3、度量分析
(1)methodMetrics
(2)typeMetrics
4、分析
本次作业一共开了7个类,其中主调度器和电梯的代码数量最多,复杂度也很大。主要是因为主调度器需要进行分配,而电梯内部需要较优的执行,所以代码的复杂度就上去了。从统计结果能看出,我这两个类里面都存在很大的方法,功能区分并不详细,这一点还需要改进
基于SOLID原则的评价
- SRP(单一责任原则)
从功能上看,其实主要包括的类是主调度器、子调度器、电梯、主类,其余的类适用于实现请求队列的存储结构的。这些类里面电梯类和主调度器的代码量比较大,方法也多,干的事比较多。但各个类之间的功能并没有怎么重合
- OCP(开放封闭原则)
在第三次作业中有三个不同的电梯,对于不同的属性,我采用的是在构造电梯时将不同的属性传进去。后来课上听老师讲解后,意识到其实可以建立一个父类,把电梯的功能部分包含在里面,然后让三个电梯分别继承父类,让扩展性得到提高。
- LSP(里氏替换原则)
这几次作业中都没有继承
- ISP(接口分离原则)
这几次作业中都没有使用接口
- DIP(依赖倒置原则)
感觉自己这几次作业在这一点上做的都不是很好,每一次加一点新的要求,需要变动的代码部分都比较多。
三、分析自己程序的bug
这三次作业在强测中都没有出现bug。简单分析一下自己课下测试中遇见的问题吧。
第一次作业只有一个电梯,同步问题只涉及到请求队列的访问,我直接粗暴的就给调度器整体上了锁,每次放请求和取请求实现同步,从而保证了正确性。
第二次作业和第一次作业其实其实差不多,所以我的同步控制都是一样的。
第三次作业的难度简直就是指数增长,代码量一下子大了很多。我写着写着就晕了,上锁的地方上的乱七八糟,在刚写完代码第一次尝试跑的时候,果然死锁了。通过在代码各个地方插入print之后,我发现我的代码,到处都是同步问题......太惨了。在第三次作业中,我的三个电梯和三个子调度器都是挂在主调度器里面的,电梯从子调度器中取请求,对于换乘的请求,电梯还会抛出请求给主调度器;而主调度器除了从标准输入接收请求,还要分配请求给子调度器,同时还要接收电梯抛出的请求。起初我想像前两次作业一样,每次上锁就直接对方法上锁,但这样的上锁操作很快就暴露的他的弊端,比如主线程给往主调度器扔请求时,若对方法上锁,则会导致整个主调度器都停止运行,包括挂在里面的三个电梯,这样会大大减慢程序的运行速度。因此后面我都改用了对某个属性上锁,比如单独对主调度器中的请求队列上锁,而不是对整个主调度器上锁。最后我的程序终于正常的运行了起来。
四、自己的测试方法
学会了使用管道,将测试文件的输出作为自己工程的输入的,达到了控制时间往程序里扔请求的目的。
通过线下自己扔一些请求进去,来一点点的找自己的bug。
五、心得体会
这是我第一次接触到多线程,该掉的坑我应该一个也没落下。
在线程安全上,我觉得我还是有一定的进步,从最开始只知道给方法上锁,到后面我学会了如何正确的给属性上锁以及唤醒。起初我的notify和wait总是没有配对好,导致线程wait后无法醒过来继续执行,但经过debug后还是找出了问题并解决了。上锁真的是个技术活,怎么上才能既保证正确性又能让程序尽量不等那么久,我觉得我还是需要进一步的学习。
在设计原则上,这三次作业其实是个由浅入深的过程,从很简单的一个电梯到后面三个不同的电梯。我觉得我还是算做到了各个类功能相互独立的,每个类只需要管好自己的东西,各个线程在保证了线程安全的前提下执行就行了。
在这个单元里,并没有过多的追求性能分,心里想的还是尽量保证正确性吧,幸好这一点也确实是做到了。
这几次作业让我收获颇多,接下来的作业里,自己也要继续加油。