面向对象程序设计与构造第二单元总结
一、第一次作业
1.思路分析
第一次作业本身难度并不大,实现的思路并不复杂,本身的坑点也比较少,但是由于我是第一次接触多线程的概念,在多线程的理解上苦苦挣扎了许久,导致第一次作业的实现过程思路比较混乱,也因此出现了一些bug。
在这次作业中我构建了四个类,分别是Main、Controller、Elevator、Waitline,事实上在本次作业中我完全可以不需要建立单独的Waitline类,完全可以把存储到来的人的容器作为一个属性直接包装在Controller类中,这样可以避免为了向队列中加入一个人时需要将Controller类作为中间者传递一个方法到Waitline类中,当时我的考虑是这样或许会使代码具有更好的可扩展性,并且这样的结构能使作业中代码的逻辑更加清晰。
这次作业中我使用了两个线程,一个是输入线程,一个是电梯线程,事实上我直接把主线程作为了输入线程,输入线程与电梯线程共享调度器这个对象,等待队列则作为调度器的一部分,这里其实考虑并不周全,更好的选择是把电梯作为调度器的一个属性,由电梯与输入线程共享等待队列这一对象。我选择的电梯策略为“新北电梯策略”,也就是每个电梯同时考虑电梯内以及等待队列中的乘客需求,先从最底层到达需要达到的最高楼层,之后从最高楼层一直向下直到到达需要达到的最低楼层(事实证明新北的电梯策略还是可以取得不错的效果的)。在我的实现中调度器只是一个摆设,所有运行策略都由电梯决定,而电梯实际上知道的事情也并不多,他做的只是到达某一楼层时检查是否有乘客需要下电梯、判断自己下一步的运行方向、检查是否需要有人上电梯。这样的设计方式为我后面两次作业代码的编写减少了许多工作,事实上在后面两次作业中我几乎不需要对电梯类的代码进行修改,但是这也决定了我设计的电梯不可能达到最完美的性能,这一点我会在后面细说。
作业中会发生冲突的地方在于输入线程要将新到达的乘客输入到等待队列中,而电梯要将等待队列中的一些人加入到电梯中,同时操作等待队列会导致冲突的出现,于是我在输入线程中对于往等待队列中加人的操作过程对等待队列加了锁,至于电梯线程,每当电梯到达一个楼层时我要做的事情其实很多,首先要检查电梯是否要在这一楼层开门,包括是否有人上电梯和是否有人下电梯,为了方便捎带我需要先决定电梯下一步的运行方向再检查是否有人需要上电梯,完成这些过程后我会先开门,之后执行上下电梯的操作,然后关门,为了避免夜长梦多,这些操作的整个过程我都给等待队列加了锁,只在电梯运行的过程把锁释放。其实这导致了一定的性能问题,比如初始时刻很多人同时到达一层需要上电梯,第一个请求到来时电梯便知道有人要上电梯,于是这个时候电梯可能会拿到等待队列的锁,这样直到电梯关上门之后其他人才能进入等待队列,至于为什么不优化,过去的经验告诉我优化越多bug越多。这样,第一次作业就比较顺利的完成了。
2.类图分析
UML类图

复杂度分析

由于运行策略在电梯类中实现,可以看出,主要的复杂度都集中在了电梯类中。
时序图分析

第二次作业
1.思路分析
本次作业新增内容是电梯由一部变为三部,第一次作业中的主要内容均保留了下来,每个电梯拥有每个电梯的等待队列,每新增一个电梯也新增一个对应的等待队列,可以看出,对于某个单独的电梯线程来说,他需要做的与第一次作业是完全相同的,而我们需要新增的内容仅仅是把新到来的人分配到不同电梯对应的等待序列中。
在思考如何分配的过程中可以想到,我们要根据一个电梯的运行状态来分配一个新到来的人,很自然的可以想到,最优选择一定是可以直接捎带的,其次如果电梯运行完当前请求之后可以马上执行这个人的请求也是一个好的选择,最不希望的就是这个人要经过漫长的等待才能坐上电梯。从这个过程中我们就可以发现,我们可以根据电梯运行的状态来给不同的请求进行打分,如果可以很方便地携带这个乘客就可以打一个较高的分数,然后每个乘客去选择分数最高的电梯即可,而且这种方式具有很多的优势,比如可以很方便的调整分配策略,并且上限非常高,可以通过设置不同的打分制度来找到一个最优的分配方法。乍一看这样的方法好像实现起来也很简单,我只需要让电梯根据自己的状态分析一个请求就可以,没有什么多余的工作需要处理。
在实现完打分的过程后我遇到了本次作业中的第一个bug,这个打分的过程我应该在什么时候进行呢,打分时电梯是要了解自己状态的,这也意味着电梯当前前进方向等特征需要是一个确定的值,于是我想到,我只要把电梯的等待队列锁住就可以了,这样电梯运行状态就不会变了,但是从上一次作业的实现过程中可以知道,为了避免多事,我把电梯开关门上下人等整个过程看成了一个整体,全都在获得等待队列的锁之后进行,这也意味着这个过程可能会时间很久,而且每个人都需要让所有电梯都给他打一个分数,每个电梯拥有锁的时间最多可以达到0.4s,每个人最多需要5个电梯最他打分,打分的过程最多可以持续两秒,也就是说一个人可能要经过比较长的时间才能知道一个电梯给他打的分数,而且所有人放在一个队列里,上一个人被分配到确定的电梯后才能对下一个人继续分配,这样需要等待的时间就更长了。这个时候我想到了另一个思路,如果我们不把所有人放到一个等待队列呢,我可以把每个人都作为一个线程,这个时候不再需要调度器,每个人都是一个调度器,他可以自己选择要进入哪个电梯的等待序列,这样就不会出现下一个人要等上一个人分配结束才能进行分配的情况。
这样,这次作业的主要思路就已经确定了,线程分为三类,分别是输入线程,电梯线程,人线程,在很顺利的完成代码编写后,我遇到了一个新的bug,也就是电梯线程应该什么时候结束。在上一次作业的实现中,如果输入结束,电梯等待队列没有人,电梯里没有人,就意味着这个电梯的工作已经完全结束了,那么电梯线程也就可以结束了,但是现在每个人都变成了一个线程,有可能出现的一种情况就是电梯等待队列没有人,电梯里没有人,输入也结束了,但是还有某个人正在绞尽脑汁思考自己应该选择哪一部电梯,如果这个时候电梯直接结束了,那就会导致这个人进入了一个不再运行的电梯的等待序列,迎接他的就是无穷无尽的等待。为了解决这个问题,我新增了一个计数器类,用来统计有多少人来乘坐电梯,有多少人已经选择好了他心仪的电梯,电梯线程的结束除上述条件满足外还要保证每个人都已经选择好了他要乘坐的电梯,至此作业的全部内容就结束了。
提交评测后在强测中我出现了一个bug,这个bug是由于我的打分方法不当引起的,这个时候的我也意识到打分是一件很复杂的事情,必须细致的考虑到每种情况才能保证电梯的正常运行,比如在Morning模式下我的打分策略导致只有一个电梯可以运行,这也导致了修改过程中的很多麻烦。
2.类图分析
UML类图

复杂度分析

由于运行策略在电梯类中实现,并且分配策略的核心在电梯类中实现,可以看出,主要的复杂度都集中在了电梯类中。
时序图分析

第三次作业
1.思路分析
这次作业新增了一些对于电梯运行的限制,不同电梯能够到达的层数不同,运行速度不同,载客量不同,其中后面两个条件在上一次作业的基础上比较容易修改,只需要修改电梯类运行速度与载客量即可,对于每个电梯只能到达特定的层数,我选择的修改方式是修改分配策略,对于任何一个特定的请求,我会安排固定的分配方式让他进行乘坐,也就是说,对于一个电梯,分配给他的请求要到达的楼层一定是这个电梯本身就可以到达的,这样就大大降低了实现过程中第二次作业代码的修改难度,我要做的只需要让请求在他需要乘坐的电梯类型中选一个最合适的即可。
这个时候还要面对一个如何换乘的问题,面对这个问题,每个人一个线程展现出来了较大的优势,我可以修改Person线程的运行方式,每个Person类都是通过一个PersonRequest初始化的,但是具体实现的过程我并没有直接把这个PersonRequest输入到电梯中,而是根据这个PersonRequest选择一种特定的换乘方式,对于换乘过程中,每乘坐一辆电梯时,生成一个合适的PersonRequest,把这个请求加入对应的电梯队列,并让这个请求处于wait状态,等到这个请求到达对应楼层时再唤醒这一请求,此时Person线程会检查这个人是否已经到达了他要到达的楼层,如果没有则继续生成新的PersonRequest重复上述过程。
这样的方案是会对性能产生影响的,每一个请求到来时都只能以特定的方式乘坐电梯,必然会导致很多时候时间的浪费,这往往出现在某些请求类似的乘客到来时,可能很多电梯都是空闲的但他执意要选择某一类型的电梯,但是由于这种方式实现起来非常简单,与第二次作业相比新增与修改的代码量很少,所以我很顺利的就完成了第三次作业。
最后第三次作业通过了强测,不过在互测时被发现了一个bug,当最后一条指令为加电梯指令并且null与这条指令同时传来时,我的电梯无法停止,在我对一个控制单元加锁后解决了这一bug。
2.类图分析
UML类图

复杂度分析

由于运行策略在电梯类中实现,并且分配策略的核心在电梯类中实现,可以看出,主要的复杂度都集中在了电梯类中。
时序图分析

可扩展性分析
把每个人作为一个线程进行分析是具有比较好的可扩展性的,这样的分配方式也与现实世界更相近,每个人都可以独立的选择要做的事,可以很方便地实现一些其他的搭乘电梯的请求。另外由于电梯的运行和分配策略是完全分离的,这也可以让我们方便的增加一些功能而不需要大规模修改原来的设计方案。这种方案的缺点在于无法将电梯运行的性能最大化。
电梯在运行过程中并不能准确知道将来要发生的事,这会导致在后面增加需求的过程中不能让电梯准确满足需要,这会对于电梯的可扩展性带来一定的限制。
经过了这一个单元的学习,我对多线程的运行机制有了一定的了解,并且理解了如何通过加锁的方式保证多线程的安全运行。在运行的过程中需要选择合理的设计方式,一定要避免轮询以及不安全线程访问对象的出现,以后面对多线程问题时应该尝试去画出线程共享对象的过程,分析每一个线程对于共享对象的操作,避免不安全操作的出现。

浙公网安备 33010602011771号