OO第二次作业

一、第五次作业+第六次作业

1.1设计策略及架构

  • 第五次作业的要求是完成“傻瓜式电梯”的电梯系统。吸取了上一个单元的经验教训,我在设计的时候考虑到了后续作业要求的可能性,我在第一次电梯作业的时候尽可能详细地将整个架构的功能进行分散化,这样有助于以后系统功能的添加与修改。
  • 在第一次作业中,我的主要思路是:读取用户输入为一个线程,电梯为一个线程,电梯通过主动访问控制台,从而控制台告诉电梯的运行方向,以此完成将所有人员送往各自的目的层的要求。
  • 在具体实现中,我创建了Floor、Passenger类,在控制台用Arraylist进行对15层楼的统一管理,并且每一个Floor中有一个Arraylist动态数组用来存储还未上电梯的人员。
  • 具体思路是:Elevator采用扫描算法。在初始状态,线程Elevator在一层等待,不断询问控制台是否有乘客需要接送,Input线程负责将输入的乘客加入到对应的起始楼层的Arraylist中。只要楼层内还有乘客需要接送,电梯通过询问控制台ControlCenter发现有乘客需要接送,电梯就开始运行,运行的方向也为Elevator主动询问ControlCenter获得的,从而Elevator有一个运行方向并且向该方向运行、Elevator每当经过一个楼层时,就会询问ControlCenter该层是否有乘客的运行方向与电梯当前的运行方向一致,由于电梯没有人数限制,因此只要Passenger的目的楼层方向与电梯运行方向一致,那么这个Passenger就会进入电梯。每当电梯运行一层时,电梯就会做出如下判断:若电梯内无人,电梯询问ControlCenter当前运行方向没有Passenger需要接送,那么Elevator就会改变运行方向。当楼内无人且电梯内无人时,电梯就会停止运行,立即在当前楼层停留等待。
  • 在调度设计上:电梯使用了扫描算法,并且电梯采用了每层楼询问ControlCenter该层是否有乘客需要上电梯的设计,这两个设计从第一次电梯作业就一直保留着,并且也适用于这三次的作业要求。减少了日后的代码修改。
  • 第五次与第六次作业电梯的运行策略不同:
    • 第五次作业采用的是电梯轮询调度器是否需要接乘客,如果不要接人就呆在当前楼层,并且不断询问电梯是否需要接乘客。
    • 第六次作业采用的是wait()和notifyAll(),当电梯不需要继续运行时,就在当前楼层等待并且进入wait状态,当有新乘客到来时,调度器会主动notifyAll()唤醒电梯,让电梯继续运行。
  • 六次作业:由于第二次电梯作业的要求仅仅增加了负数楼层以及稍微智能的调度,对于我第五次作业采用的扫描算法来说完全能够满足要求,因此我第六次作业中全部沿用了第五次作业的架构,仅仅修改了楼层的数目,增加了负数层。

1.2基于度量的分析

1.2.1时序图分析:

  • 由于本次作业只有一个电梯,不会产生多个消费者对同一个资源进行争夺的问题,我没有采用wait()和notifyAll()策略,而是采用while()循环不断询问的方法,这样导致CPU运行时间开销大。
  • 对于线程结束的控制:我是采用了设置标志位的方法,当INPUT()线程结束时,我会在ControlCenter中将flag设置为TRUE,表示没有新的乘客了,当电梯将整栋楼的乘客运送完并且询问调度器时,调度器可以告诉电梯此时可以结束Elevator线程了。
  • 第五次作业采用的是电梯轮询调度器是否需要接乘客,如果不要接人就呆在当前楼层,并且不断询问电梯是否需要接乘客。
  • 第六次作业采用的是wait()和notifyAll(),当电梯不需要继续运行时,就在当前楼层等待并且进入wait状态,当有新乘客到来时,调度器会主动notifyAll()唤醒电梯,让电梯继续运行。

1.2.2类图分析:

1.2.3基于度量的分析

 

类复杂度分析:

  • 从图中可以看出,Elevator.java代码复杂度最高,代码数比重最大,ControlCenter.java次之,这也符合设计的基本思路,即:电梯负责接送乘客,控制台负责处理电梯运行方向、电梯的运行状态(等待\运行)、电梯的查询请求处理。
  • 由于Elevator线程采用的是轮询策略,因此方法中有较多的while()、if()条件判断函数,这也导致了Elevator类的复杂度上升。因此可以将条件判断的方法向控制台转移一部分,这样可以作为一个优化的方向,使得代码更加平衡。
  • 这一次的作业要求比较简单,所以代码以及方法数都不是很多,主要是熟悉线程并且使用线程。

 1.3分析bug以及性能优化

  • 高工没有互测。
  • 第五次次作业在公测中没有发现bug,因为没有性能分的要求,因此是满分。
  • 第六次作业代码都运行正确,最后总得分为88.9分,性能分丢失了较多的分数。
  • 在完成作业的过程中,遇到的bug主要是:
    • 电梯采用的是扫描算法,因此对电梯运行方向的改变容易出现问题,我的电梯在一开始会改变不了方向,后来调整了改变方向的条件解决了
    • 电梯在一开始位于一层等待,当来了一个新乘客并且该乘客的起始楼层为一层时,我的电梯会先上一层楼,到了二楼后再下来接这个人。这个bug的主要原因是:条件判断不准确,后来修复了。 
  • 第六次作业中由于增加了负数楼层,而我采用的是扫描算法,经过试验后发现,将电梯的初始方向设置为向下能提高电梯的运行性能,这样做的目的是为了让电梯尽可能从一头开始扫描。当负数楼层和正数楼层在一开始同时到来时,电梯会先向下接负数楼层的乘客。

二、第七次作业

2.1设计策略及架构

  • 第七作业增加了较多的要求,设置了三台电梯,每台电梯能够停靠的楼层不完全相同,电梯每运行一层的时间不同,电梯的最大载客数目不同,因此需要对第六次作业做出一些功能上的完善,但是架构依然沿用了前两次的扫描式算法,每台电梯向调度器询问请求,调度器负责管理每台电梯的运行,主要的难点在于优化的处理,如何处理好三个电梯调度上的关系,这是一个非常值得注意的问题;还有一个难点在于乘客如果不能直达,该如何换乘的问题,这个问题的解决影响着最终的性能甚至影响程序的正确性。
  • 主要思路:
    • ControlCenter:实现当电梯询问调度器是否需要继续在这个方向运行时的应答,实现将电梯的信息传达给每一层楼,以及实现判断电梯是否需要结束线程。
    • Elevator:使用扫描式算法,会主动将电梯最新运行至的楼层告诉给ControlCenter
    • Passenger:从起始楼层Floor获得每部电梯当前所在楼层的信息,从而判断乘坐哪部电梯理论用时最短。
    • 对于换乘的解决:我的设计思念是将换乘乘客都尽可能集中在同一个楼层,这样电梯来的时候就能够一次性将多个人带走,相较于将乘客分散在不同楼层,这将减少电梯开关门的次数,理论上带来性能的优化。
    • 对需要换乘的乘客进行硬编码,即在乘客出现时即规定该乘客的换乘楼层,在换乘楼层下电梯后再将该乘客加入到换乘楼层所在的等待队列。
    • 每个乘客在刚出现时,即可以知道这个乘客可以搭乘哪些电梯直达,因此,一个乘客可能可以搭乘多部电梯,这也是可以进行优化的策略。
    • 对于每个乘客,他一开始加入起始楼层的等待队列时,由于三个电梯都把他们所在楼层告诉了每一层楼,因此乘客可以计算从等待电梯开始至到达目的楼层所花费的理论时间,这样每个乘客理论上就找到了耗时最短的电梯并且按下这个电梯的按钮,从而实现性能优化。
    • 每层楼有六个按钮,分别对应着:
    1. A电梯向上走
    2. A电梯向下走
    3. B电梯向上走
    4. B电梯向下走
    5. C电梯向上走
    6. C电梯向下走
    • 当电梯运行时,会告诉调度台自己当前所在楼层和运行方向,为了避免乘客进行死等的现象发生(即:乘客在1层等待电梯A,而电梯A正在二层向上走去20层接人,如果不避免这种情况,最坏的可能是:A电梯运行到第20层,再返回1层接人,这将导致很大的性能损失),调度台会告诉与电梯运行方向相反的楼层,该电梯正在向某个方向走,每层楼的乘客看看需不需要按其他电梯的按钮避免死等这部电梯。
    • 当这栋楼任何一层都不需要某部电梯时(即每一层该部电梯的两个按钮都没有亮),那么该电梯将呆在当前楼层进入wait状态。
    • 设置按钮的主要作用是:主动将处于wait()状态的电梯唤醒,当某一层的乘客主动需要这部电梯时,电梯才不会继续进入wait状态。
    • 每当电梯经过一层时,都会主动询问调度台,这一层是不是有人可以搭我的电梯,并且告诉调度台,当前电梯内的乘客有都想要去哪些楼层,并且让哪些与电梯内有相同目的楼层的乘客优先进入电梯(这个的目的是因为一部电梯的载客数目是有限的,应当尽肯能地将让有相同目的楼层的乘客进入电梯,这样可以减少电梯开关门次数,从而优化性能)。
    • 每当电梯运行了一层,这部电梯都会notifyAll()尝试唤醒其他电梯,因为可能电梯装满了人,而其他电梯可能可以将未上电梯的乘客送达目的地,从而优化性能。
    • 每当一位新乘客加入到起始楼层队列时,该层楼都会检测:想要搭乘某部电梯的人数是不是超过了荷载(例如A电梯可以搭载6人,而15层已经有7个人在等待搭乘A电梯了),这个时候就会让可以选择其他电梯的乘客按下其他电梯的按钮,从而提早让电梯唤醒,而不用等待该部电梯到达该楼层发现一次性带不走所有人后再唤醒其他电梯,这样也可以提升性能。
  • 总思路:三部电梯为三个线程,Input为一个线程。每部电梯都采用扫描式算法的策略。三部电梯都会在途径每一层楼时询问是否有乘客可以坐这部电梯到达目标楼层,但是电梯处于wait()状态时,只有当至少有一层楼该部电梯对应的按钮亮了至少一个的时候才会结束wait状态,调度器负责处理电梯的运行方向的问题,告诉电梯是否需要改变方向,因为该运行方向上的楼层已经没有乘客按下电梯。当整栋楼都不需要某部电梯时(即任何一层都没有按下该部电梯的其中之一)电梯在当前楼层停下,进入wait状态。

2.2基于度量的分析

2.2.1时序图分析:

  • 本次作业有三个电梯(三个消费者),将产生对同一个乘客(资源)的争夺,因此锁的设置将至关重要,如果没有设置好锁,将导致不必要的资源占用或出现资源占用冲突异常。
  • 对于线程结束的控制:我是采用了设置标志位的方法,当INPUT()线程结束时,我会在ControlCenter中将flag设置为TRUE,表示没有新的乘客了,当电梯将整栋楼的乘客运送完并且询问调度器时,调度器可以告诉电梯此时可以结束Elevator线程了。
  • 每部电梯每当运行一层后都会唤醒其他电梯,其他电梯被唤醒后若发现某一层对应该电梯的按钮被按下,将进入运行状态,否则继续进入wait状态。

2.2.2类图分析:

 

 

2.2.3基于度量的分析

 

类复杂度分析:

  •  可以从中看到,复杂度最高,代码最长的为Floor.java类,这是因为许多优化函数都在这个类里面。我所做的优化主要是三个电梯直接协同的优化,这就需要每一层的六个电梯按钮精准地按下,这样才能让电梯的调度更加高效,这就无可避免地导致了Floor类复杂度的升高。
  • 而ControlCenter、Elevator、Passenger三个类的复杂度相近可以从三个类的主要功能看到,相较于第五次、第六次作业复杂度、代码量的上升,都是因为优化的实现,将电梯信息及时更新给调度器以及每一层楼,以此帮助乘客精准地按下电梯按钮,从而实现性能上的优化。
    • ControlCenter:实现当电梯询问调度器是否需要继续在这个方向运行时的应答,实现将电梯的信息传达给每一层楼,以及实现判断电梯是否需要结束线程。
    • Elevator:使用扫描式算法,会主动将电梯最新运行至的楼层告诉给ControlCenter。
    • Passenger:从起始楼层Floor获得每部电梯当前所在楼层的信息,从而判断乘坐哪部电梯理论用时最短。
  • 可以从三个类的主要功能看到,相较于第五次、第六次作业复杂度、代码量的上升,都是因为优化的实现,将电梯信息及时更新给调度器以及每一层楼,以此帮助乘客精准地按下电梯按钮,从而实现性能上的优化。
  • 代码分配不均衡的主要原因还是优化思想上的问题,我的优化思路重点在于乘客,因此就需要每一层楼知道电梯的实时信息,帮助乘客作出按下按钮的判断,实际上还可以从调度的方面进行优化,从而让ControlCenter分担Floor的代码量,以及让Elevator进行自我调度优化,使得整体代码分配更加均衡合理。

 2.3分析bug以及性能上的优化

  • 高工没有互测。
  • 在第七次作业的公测中没有发现bug,强测最终得分为93.19分,性能上损失了较多的分数。
  • 在完成这次作业的过程中容易出现的bug:
    • 锁的使用容易出现问题,一开始设计的时候,对锁的使用不准确,导致了无谓地占用资源,使得可以运行的电梯无谓地等待。
    • 电梯从wait状态唤醒的条件判断不准确,应当在有某一楼层按下了该电梯按钮的时候再进入运行状态,不能一来新的乘客就无条件唤醒,因为这个乘客可能坐其他电梯能够更快到达,导致该电梯空跑。
    • 电梯线程结束的条件不准确:一开始还是沿用了上一次作业的条件,当整栋楼没有乘客时就结束程序,这是不对的,因为可能出现换乘乘客还在电梯里面,而另外两部电梯发现没有新乘客来并且楼层里面没有人就结束线程,导致需要换乘的乘客到达换乘楼层后无电梯可坐,从而使得任务失败。正确的做法是应当加上当其他两部电梯都处于wait状态的条件,这个时候才能结束电梯线程,从而保证换乘乘客也能到达目的地。
  • 性能上的优化:我的优化思路重点在于乘客,因此就需要每一层楼知道电梯的实时信息,帮助乘客作出按下按钮的判断,实际上还可以从调度的方面进行优化,从而让ControlCenter分担Floor的代码量,以及让Elevator进行自我调度优化,使得优化效果得到进一步提高。但是由于每一种调度策略都有极端情况使得该种调度策略弱于其他调度策略,所以没有最好的优化策略,只有在大多数情况下相对较优的调度策略。
  • 第七次作业中由于增有负数楼层,而我采用的是扫描算法,经过试验后发现,将电梯的初始方向设置为向下能提高电梯的运行性能,这样做的目的是为了让电梯尽可能从一头开始扫描。当负数楼层和正数楼层在一开始同时到来时,电梯会先向下接负数楼层的乘客。

三、总结

3.1总体框架问题

  • 框架上的设计由于吸取了上一个单元多项式求导的经验教训,这一次我在第一次电梯作业的时候就仔细考虑了整体框架的架构问题,在第一次电梯作业时,我就构建了电梯Elevator类、调度台ControlCenter类、楼层Floor类、乘客Passenger类,使得后续两次作业完全没有改变架构,由于第一次已经实现了电梯扫描算法进行调度,这也完全适用于后续两次作业,因此后续两次作业也基本上没有改变电梯调度算法的架构。所以,我的架构在后两次作业中基本完全沿用了第一次作业的架构,实现的只是针对性的优化问题上的功能扩充,这使得第二次电梯作业基本上没有修改很多代码,第三次电梯作业只是增加了相应的功能而已。因此,在代码架构上这一次相较于多项式作业每一次都是全新的架构来说有很大的进步。

3.2SOLID原则

  • 单一责任原则:每个类都有自己的职责,Elevator类负责扫描算法的实现,ControlCenter类负责电梯的调度,Floor类负责管理该层乘客。梅格磊都有独特的工作,彼此没有互相干涉。
  • 开放封闭原则:代码更新无需删除原来的代码,由于第一次架构做到了比较全面的考虑,因此后两次作业是在第一次作业的基础上进行功能的扩充以及进一步的优化。
  • 里氏替换原则:没有使用继承。
  • 接口隔离原则:没有定义接口,在本次作业实现上没有必要。
  • 依赖倒置原则:没有使用抽象类。

3.3bug修复

  • 三次作业强测中测都没有出现wrong answer,但是第二次以及第三次作业的性能分上有所损失,我认为还可以在性能上有进一步地提升。
  • 平时作业中遇到的bug在前文已经叙述过了。
  • 高工没有互测。

3.3线程安全

  • 线程安全的实现最主要的依赖于锁的使用,锁的使用容易出现问题,对锁的使用不准确,将导致了无谓地占用资源,使得可以运行的电梯无谓地等待。
  • 我在三次作业中均是电梯为线程,而调度器不是线程,电梯通过访问调度器并且把电梯的状态告诉调度器,调度器告诉电梯调度策略。
  • 我在第一次作业的时候采用的方法是给每一个方法加上synchronized,但是在第三次作业中发现这种策略会导致多线程根本无法运行,因为当一个线程抢占了资源后,其他线程根本无法获得该资源,因为我只设置了一个锁,三部电梯都需要争夺这个锁。因此我改变了策略,在电梯需要该资源时再抢占这个锁,当不需要时(例如开门关门不需要占用该资源)就释放这个锁,使得其他电梯可以顺利运行。

3.4心得体会

  • 在这一个单元的学习中,我收获了很多,学会了如何进行多线程操作,如果保证线程安全等等。
  • 在架构方面,我觉得我比上一单元做的更好,这也算是一种收获,通过对上一单元的总结,我在这一个单元第一次作业的时候就认真考虑到后续任务可能出现的要求,因此在第一次作业就将架构尽可能地实现完备,也正是因为如此,我第二次、第三次电梯作业都完全沿用了第一次作业的架构,只是在功能上进行了扩充以及在优化方面做得更详尽。
  • 需要与同学多交流探讨,这样有助于架构在一开始构建的时候更加完备,避免重构。

猜你喜欢

转载自www.cnblogs.com/Char1ie/p/10753126.html