zoukankan      html  css  js  c++  java
  • OO第二单元总结

    OO第二单元——多线程总结

    2019-04-23


     前言

      OO多线程电梯的爆肝之旅终于告一段落了。也许是因为前两次安全划水,第三次出现了一些坑(感慨多线程果然是玄妙无比 :-)

      接下来就从设计策略、复杂度分析、设计原则等方面谈谈历次作业的情况,最后讲讲测试和一些感想。

     


    第五次作业:单部电梯傻瓜调度(FAFS)

    • 类图

      与第二次作业基本一样(只是采取策略不同)就不放了(且过于zz)

    • 时序图

        

    • 设计策略

       感觉没啥好说的……设置MainElevatorScheduler三个类,主线程负责获取输入请求交互给调度器;调度器内部存一个请求队列(为保证线程安全用了concurrent包里的阻塞队列LinkedBlockingQueue,且不用手写同步方法),核心方法就是接收主线程请求和分配请求给电梯(由于采取傻瓜调度策略只需无脑分配即可);电梯设置成一个线程,run方法里不断执行“从调度器取一个请求”、“执行请求”二步即可。唯一需要注意的就是线程的退出问题,不能在电梯还未执行完请求时就退出呀(电梯关人事件 :-)),这里就需要设置一个退出标志:输入完毕+所有请求执行完毕,嗯。

    • 复杂度分析  

       由于此次傻瓜调度策略逻辑较简单,整个工程的方法复杂度和类复杂度都不高,主要是电梯线程的run方法,几乎把电梯的全部运行逻辑都放在里面实现(其实可以从中抽取几个模块出来设成新的方法,run方法不会过于冗长,代码风格也会比较好看)

    • SOLID原则
      • 单一责任原则:主类、调度器类、电梯类分别负责请求的输入、分配、执行,类职责较为单一。略有不足的是电梯类的run方法可以继续进行拆分(例如把上下楼分割成上下一层的原子操作等)。
      • 开放封闭原则:关于扩展性,可以说是,毫无扩展性。因此第二次作业大整改 :-)
      • 里氏替换原则:无,未用到继承
      • 接口分离原则:无,未用到接口
      • 依赖倒置原则:请求输入、电梯、调度器逻辑分离,相互间依赖关系分明
    • 关于测试
      • 强测:安全
      • 互测:安全(大家一起愉快放空刀~) 

         这次实在没什么好说的(捂脸)


    第六次作业:单部电梯智能调度(ALS改)

    • 类图

     

      很抱歉依然是ugly的三类打天下……

    • 时序图    

    • 设计策略

       延续了第一次作业的架构,但是第二次的电梯比第一次更智能一些呢(要求实现捎带),不过这次我并没有完全采取指导书给的策略,而是以指导书为基础实现 “最大限度的捎带” 

      在这次的调度器里,我将所有向上和向下的请求分别存在两个阻塞优先队列(PriorityBlockingQueue)中,upQueue以起始楼层升序排列,而downQueue以起始楼层降序排列。依旧是主线程负责请求的输入;调度器接收来自主线程的请求,并实时接收电梯的状态信息(电梯当前所在楼层,电梯当前目标楼层,电梯当前运动模式),核心方法有三个:

      putRequest() 负责接收请求并分类放置到两个请求优先队列之一。

      getMainRequest() 给电梯分配一个主请求,分配的依据是:观察当前向上和向下的请求的数目,选取较多一方的作为“一趟”的运行方向;选取选择的请求队列的队首元素作为主请求(即为实现一趟“最大限度的捎带”)。

      getPickyRequests() 给电梯分配捎带请求,根据电梯当前所在楼层和运行方向尽可能多的分配可稍带请求,并且修改电梯这“一趟”的目标楼层为距电梯当前运行最远的楼层。

    电梯仍然作为一个线程,增加了开关门、上下一层楼(注意负楼层)、乘客进出等原子操作。内部依旧存了两个上、下的阻塞优先队列,只不过这次优先级设为乘客的目标楼层。运行以“一趟”作为基本单位:获取主请求、运行、每运动一层从调度器获取一次捎带请求,同时也将目的地为当前层的乘客从队列中放出。运行条件为当前楼层不等于目标楼层。

      虽然在一些特殊情况下效率可能并不如纯ALS策略,不过从平均意义来看,效率还是高于纯ALS的(强测分数也证明了这一点)。

    • 复杂度分析  

       由此次分析可以看出,调度器的分配捎带请求的方法、电梯的run方法复杂度还是偏高(或许还可以拆分出几个小模块来)。

      关于类复杂度,电梯的设计耦合度有点超标了,电梯的设计与管理还欠考虑。

    • SOLID原则
      • 单一责任原则:同上,主类、调度器类、电梯类分别负责请求的输入、分配、执行,类职责较为单一。不足的是调度器类的调度算法还有改进空间,分配请求的方法还可以继续拆分,电梯的run方法同理。
      • 开放封闭原则:关于扩展性,这次海星,第三次作业基本复用了这次的代码。
      • 里氏替换原则:无,未用到继承
      • 接口分离原则:无,未用到接口
      • 依赖倒置原则:请求输入、电梯、调度器逻辑分离,相互间依赖关系较为分明
    • 关于测试
      • 强测:安全
      • 互测:安全(同组dalao太强了于是只能继续放空刀)

      不过值得一提的是这次误入了一个神仙屋子(瑟瑟发抖),同组dalao的代码要性能有性能、要架构有架构、简明好看清晰可扩展性强(再看看自己写的啥玩意儿 :-)……)

      然后也学到了一些小trick(如sleep的精准控时操作、工厂模式、ReentrantLock实现手动加锁等等),嗯,学到了学到了( •̀ ω •́ )✧ 


    第七次作业:多部电梯智能调度(SS)

    • 类图

     

       三部电梯统一建模(通过传入电梯每层楼运行时间、最大载客量、电梯类型(字符串)的构造方法进行构造);每部电梯有一个子调度器与之进行交互,分配请求(即上一次作业的ALS调度器);设置一个全局调度器,记录了每部电梯可停靠楼层的情况,并依据一定的原则将请求分配给子调度器。分级调度器使得调度不显得那么臃肿,更为清晰;由于输出接口可能会有线程不安全的情况所以为输出专门设置了一个线程安全的OutputHelper类。整体大概是这样:

    • 时序图    

     

    • 设计策略

       电梯与子调度器的交互基本完全复用上次作业,故不再详述。

      讲讲全局调度器的设计,其实相当于在上次作业的基础上多加了一个中间层,使得调度逻辑显得不那么臃肿。我在全局调度器里存了五个阻塞队列,一个可直接分配请求(无需换乘)的队列serveQueue,一个存储拆分后的第二换乘请求的transWaitQueue以及三个因为电梯载客量满等待分配给三部电梯的waitQueueA/B/C。那么来了一个请求该怎么处理呢?有意思的是,我在全局调度器里存了6个静态List,命名为a、b、c、ab、bc、abc,分别存储只有电梯A能到达、只有B能达到、只有C能到达、只有A与B能到达、只有B与C能到达以及ABC都能到达的楼层号;并且,这6个List都有自己的编号,a是001,b是010,c是100,以此类推。

      关于判断请求是否换乘: 来了一个请求,我会依据它的起始楼层和目标楼层各自返回一个List编号,相与&一下,若为0则说明无法直达必须换乘;否则说明无需换乘,直接存入serveQueue队列。

      关于换乘请求的处理: 课下我根据请求的运行情况列了一张表,对所有需要换乘的请求进行了分析(其实还是有一定规律的),按照规律将换乘请求一分为二即可。第一请求存入serveQueue队列去执行,第二请求存入transWaitQueue队列。这里有一个处理数据冲突的问题:必须执行完换乘第一请求后、才能执行换乘第二请求,因此transWaitQueue中请求的分配必须要对每部电梯及其子调度器的队列进行相同id的查询,没找到相应id才可以进行分配。

      关于请求的分配:设置了三部电梯各自的等待队列(类似缓冲区?),同样利用请求的起始楼层List编号和目的楼层List编号进行相与&,并依据相与结果进行请求的分配:需求电梯载客量未达上限,则分配给相应子调度器;否则,存入相应等待队列。等待队列中请求分配的条件是“当前电梯载客量未达上限”。

    电梯A、B、C分设三个线程,由于未把调度器设为线程(一方面也是担心线程安全出问题),电梯线程的每次执行在上次作业的基础上,最前面加上了全局调度器分配请求的相应方法(说实话很不美观<(_ _)>)。此外由于多加了一层调度器,电梯线程的退出逻辑也要进行相应的修改。

    • 复杂度分析  

       这次作业的工程量显然增加了不少,方法复杂度最高的还是电梯的run方法以及调度器类的调度方法,几个用于查询的方法复杂度也偏高。

      果不其然,全局调度器和电梯的类复杂度都爆了……毕竟还是延续上一次的基础,整体的架构设计还有不合理的地方(包括全局调度器对于换乘的静态处理,并不适用于比较复杂多变的现实情况)。

    • SOLID原则
      • 单一责任原则:主类、调度器类(主/分)、电梯类依然分别负责请求的输入、分配、执行,类职责还比较单一……当然主调度器和电梯类的一些较高复杂度的方法其实可以进行进一步的拆分。
      • 开放封闭原则:关于扩展性,若要增加新的电梯型号或者修改停靠楼层,需对全局调度器的请求处理(尤其是换乘请求)进行全方位大整改……很大原因是对请求的静态处理,因此这点做得不到位。
      • 里氏替换原则:无,未用到继承
      • 接口分离原则:无,未用到接口
      • 依赖倒置原则:全局调度器和子调度器之间依赖关系较为明晰,子调度器和电梯之间的交互也海星,就是在电梯的run方法里还调用了全局调度器用来分配请求的静态方法就……可是不把调度器设成线程进行实时交互我好像只能想到这种方法(还是太菜orz
    • 关于测试(哭了
      • 强测:这次很不幸地(由于玄妙的线程安全问题)翻车了,被爆了一个点
      • 互测:大概是进了C组,大家一起互相伤害 :-),被hack了两次,自己也疯狂hack别人,发现的别人的bug有:十分神秘的stackoverflow(?)、执行请求超时200s(因为无法实现捎带只能vip调度??虽然安全性是完美保证了……我还是不知道这位朋友到底怎么过的强测)、换乘出现问题(很多,应该是线程安全出了差错)
      • 关于我的bug:共同点在于,都是换乘拆分后的第一第二请求出现了交叉执行的情况,即一个人在同一层楼进出两次(电梯幽灵?)。很神秘,在本地进行测试的时候,强测被爆的数据以及被hack到的互测数据1并没有出现错误,包括什么也没有改动的情况下交上去就能修复两个bug :-),所以说这种不可复现的bug真的是……倒是被hack的互测数据2是唯一一组能复现bug的数据,在调试的过程中还有很奇妙的情况是加了几句调试用的输出语句反而对了?大概是输出延迟累积误差的原因(所以放到err流才是正解) 不过说实话在调试时我还是很难理解为什么会发生这种情况,后来进行了各种尝试,发现自己对阻塞队列的一些操作使用不当(要阻塞应当用put和take方法)、未判断清几个共享资源的方法(即便是一写多读也应加上同步控制)以及,对实质上只含一个元素的队列(全局调度器的serveQueue)的非空判断(用if代替while,这点很神奇),修改后才基本不会出现这种错误情况,只能说自己对线程安全的理解判断还不够细致,自己本地测试得也不够多、不够全面。 

     关于这几次作业的测试手段

       前排鸣谢各位dalao分享的测试数据生成器+定时投放+正确性结果检验,实现了半自动流水人工评测 :-)

      互测时(包括自测也是)基本就是生成随机数据 → 打jar包定时投放输出 → 结果比对这么一个流程叭,然而自动化程度较低导致测试效率并不高,自测的时候也没能测出自己的bug(很绝望

       前两次互测的时候bug确实难找(最后都放空刀了),到了第三次(也是自己翻车的原因)基本随机一组数据一炸一个准……没遇到其他同学说的本地测出bug交上去变空刀的神奇情况,不过讲真多线程这一单元真的很玄妙,而且课程组提供了法定输入输出接口杜绝了WF的情况,不同于第一单元,这一单元测出的更多是设计逻辑方面的一些考虑以及线程安全等等的问题(大概也更OO了吧)。


     一点感想

    • 其实遇到问题、解决问题真的是很有效的学习手段,本人因为在前两次作业中侥幸没有遇到线程方面的各种问题(据我所知其他同学多多少少有遇到如本地可输出评测无输出等一些奇奇怪怪的情况),导致对线程运行顺序和安全方面理解不深,第三次作业就不幸踩坑了。这么看来,没遇到问题反而是一种不幸(毕竟缺少了相应经验)。 
    • 关于线程安全,真的玄妙无比。其实本人到现在都不知道调试无法复现的bug的手段(研讨课上被同学问了,很尴尬,感觉只能加大测试量?求教)对于引发线程不安全的本质:资源的分配与共享,一定要进行细致的分析,不要错漏任何一个共享资源可能引起数据冲突的方法;其次对于java提供的一些数据结构,应该在深入了解的基础上才使用
    • 关于设计原则,一个好的架构设计必然是职责单一、可扩展性强、依赖关系分明的。在本单元的电梯作业中,如何实现职责单一的请求输入类、调度器类和电梯类一直是核心所在,尤其是第三次作业,没有一个合理的职责划分会导致线程运行逻辑不清,也会增加debug的难度。关于可扩展性方面感觉自己一直做得不够好(真的不是次次重构次次爽的啊!)。关于依赖关系,相互交错调用的对象之间依赖关系交叉,出现bug的几率也会大幅度提升,因此合理的关系分类也是十分重要的(wtcl)。

      综上,很希望能观摩一下dalao们的代码,是如何进行合理的分工设计的(以及真的很好奇的第三次作业的优化方法,想不出三部电梯怎么实现互相协作以提高效率)

      嗯,下一单元继续加油鸭!

  • 相关阅读:
    [五]SpringMvc学习-Restful风格实现
    [四]SpringMvc学习-对servlet与json的支持与实现
    [三]SpringMvc学习-封装、乱码问题、重定向、转发
    Android-aidl, binder,surfaceview
    linux memory dump--http://www.forensicswiki.org/wiki/Tools:Memory_Imaging
    Vanish/squid
    dongle --NFC
    词频统计 in office
    各种小巧的Hello World
    程序入口函数和glibc及C++全局构造和析构
  • 原文地址:https://www.cnblogs.com/Ace424/p/10752803.html
Copyright © 2011-2022 走看看