第二单元的三次做业是颇有特色的三次做业。多线程电梯的设计思路和前两次电梯做业迥然不一样,致使我花费了大量的时间去重构以前的代码,使其适应多线程电梯的做业要求;文件监视器是一个独立的做业,不像电梯和出租车那样是一个系列,所以写起来没什么包袱,感受并不困难;出租车调度和多线程电梯写起来感受比较类似,但出租车几乎没有算法上的难度,所以主要的工做都花费在了如何构建一个好的设计上面。这三次做业之间看起来没有什么关联,但却环环相扣,一步一步加深着我对多线程编程的理解。算法
我对这三次做业的整体难度评价为:多线程电梯 > 出租车调度 >= 文件监视器。(这个难度基本上是根据个人熬夜时间来判断的) 编程
之因此排出这样的难度顺序,是由于多线程电梯和出租车调度有着一个共同的难点,而这个难点是文件监视器所不具有的——程序的运行时间须要与这个世界的真实时间保持同步。这是这两次做业的一个大坑,也是我在好几个深夜里不睡觉而被迫面对着电脑屏幕的罪恶源泉。虽然多线程极大地加强了用户与程序交互的即时性,可是为了同时保证交互的即时性和逻辑正确性,编程者须要付出许多额外的努力和工做。数组
电梯系列做业是让我写得很不爽的三次做业。第一次的傻瓜调度,我设计了一套我自认为十分精妙的判断同质的算法,从而几乎没有阻力地无伤经过了公测和互测;但到了第二次ALS调度,噩梦就开始了:我发现本身的傻瓜调度算法彻底没法移植到ALS上面,于是不得已更换了算法,并大面积重构了程序;到了多线程电梯,我又一次痛苦地发现,以前的ALS调度算法与多线程电梯的即时输入是不相容的,只好被迫又一次地重构。三次做业,三套算法,三种设计,若是有一我的连续三次分配到这样的代码,恐怕他根本不会认为这三次做业都出自同一人之手。早知如此,我在第一次电梯做业就应该使用模拟爬楼的算法,这样就不会有后面这么多糟心事儿了。安全
抛开这些悲伤的过往不谈,个人多线程电梯采用了与老师总结课上PPT类似的设计:当一条请求输入进来以后,会被发送到一个总请求队列中。主调度器根据当前三部电梯的情况,把这条请求派发到合适的电梯中去。每个电梯保有一个本身的小请求队列和小调度器,主调度器派发的请求进入某一部电梯的小请求队列以后,会由这部电梯的小调度器来判断是否须要进行捎带。这样设计的好处是,把判断同质的过程和判断捎带的过程分离,将一个大的调度器类拆成两个调度器类,从而减小调度器类的代码量。数据结构
此次做业遇到的一个难题是:怎样让电梯精确地知足"运行一层楼花费3.0s,开关门一次花费6.0s"。由于是第一次接触多线程编程,对sleep和wait的用法还不太熟悉,为了保证公测可以经过,我采用了模拟时间的方法,即输出的是所谓的"电梯系统时间",是假的、事先计算出来的,而非直接取自系统时间。在电梯运行的过程当中,让电梯线程sleep三秒钟或者六秒钟,以使模拟时间和真实时间同步。固然,既然使用了这种方式,就势必面临着时间差的问题。我解决这个问题的方式是:在电梯线程的无限循环里面,每一次循环体开始的时候先获取一下当前的系统时间,到循环体的最后判断一下已通过去了多少毫秒,并从睡眠的时间中把这个数字减掉。经过使用这种方式,个人程序运行得还算精准,总体偏差不会大到一个不可接受的程度,互测中很难被发现与此相关的Bug。多线程
本次做业的经典OO度量状况以下:架构
可见多线程电梯的实际代码量并很少,只有1000行左右(这其中还包含了实质上并无被用到的ALS调度器和傻瓜调度器)。可是因为第一次使用多线程编程,对run方法和临界区域还不太熟悉,致使在电梯线程里的代码嵌套层数过多,如上图中红字所示。并发
本次做业的类图以下:性能
从雷图中能够看出来,本次做业在设计上存在着过分封装的问题。为了知足同步控制的要求,我在电梯类以外建立了一个Elevators类,其中用数组将三个Elevator类的实例包含在里面,调度器只能与Elevators类进行交互,而不能直接访问某一部电梯。这样作看似合理,但其实是彻底没有必要的。过分的封装使代码变得丑陋和臃肿不堪,须要无数个getter和setter才能完成所有所需的操做,这毫无疑问对代码质量是有害的。此外,因为惧怕线程安全问题,我对Elevators类中的几乎全部方法都使用了synchronized标识,这样作虽然加强了程序的线程安全性,却极大地损害了并发性,同时至关程度上下降了性能。这些都是在以后的做业中须要改进的地方。学习
本次做业的时序图以下:
此次做业的线程协做设计较为合理,主调度器将请求派发至各个电梯保有的小请求队列,并在内部进行捎带判断,这极大地减轻了主调度器的工做量。
文件监视器做业是我认为本身写的比较顺利的一次,各类功能都很完备,也没有被别人挑出什么Bug。我想一方面缘由是,此次做业的指导书规定不够明确,Readme的做用被无限放大,致使任何事情只要在Readme里提一句,就可让对方没法扣本身分。例如,设计者甚至能够强制要求测试者在两次文件操做之间加入间隔,这使得程序的算法难度几乎降为0,甚至失去意义。再者,指导书明确规定,两次文件扫描操做间隔内不容许对同一个文件实施两次或以上的修改,这也很大程度上让此次做业变得很水。
文件监视器的主要训练目标是让同窗们可以作出一个线程安全的设计,但并无强调对于性能的要求,这是我认为此次做业一个很大的不足。若是没有性能要求,设计者彻底能够把全部的方法都加上同一个锁,这样就能够保证不会出现资源争夺的现象。可是这样作对学习是没有帮助的,甚至是有害的,我以为在下一届的课程中,应该对文件监视器的性能有着更高的要求。
致使此次做业难度不大的另外一方面缘由是,文件监视器并无时间上的要求,即程序的时间不须要与外部真实时间保持同步。所以,设计者能够采用各类手段使本身的程序知足指导书中规定的要求,即便这些手段是以性能的损失为代价的。整体来说,文件监视器是一个很独特的做业,既不承上也不启下,大概能够算做是两次系列做业(电梯和出租车)之间的一个小插曲。
本次做业的经典OO度量以下:
从经典OO度量中能够看出,本次做业的代码规模控制得很好,只有752行,且各类方法调用的嵌套深度都保持在一个合适的范围内。图中的红色警告是main方法,这是由于我将记录Detail和Summary的线程以匿名内部类的方式直接写在了main方法里,因此致使块调用深度大于均值。
本次做业的类图以下:
文件监视器的设计难度并不大,各个模块之间的层次也比较清晰。我设置了一个Snapshot类不断捕获文件结构快照,并在其内部对新旧两次快照进行对比,从而判断是否有文件发生了变更。在数据结构方面,我选择了HashMap而非树形结构,由于对于这次做业的要求(不须要比对文件夹,只须要比对监控区下的全部文件)来说,树形结构的性能并非很好,远远比不上HashMap的效率。
本次做业的时序图以下:
可见程序总体的逻辑并不复杂,无非就是在一个无限循环中不断捕获快照并进行对比。
相比于文件监视器,出租车调度要难写得多。这个难写不在于其算法,而在于出租车的要求多且杂。最使人痛苦的一个要求是一辆车移动一格的时间必须严格保证为200ms,这几乎就直接限制了程序的时间方式,即必须采用模拟时间,而后让程序的sleep时间向模拟时间靠拢。为了解决这个问题,我采用了sleepUntil方法,即先计算出租车应该在何时到达,而后再让程序睡到那个时间。这样作虽然有一点点耍赖,但确实很好地完成了指导书中的要求。
此次做业是系列做业,所以须要一开始就打好一个设计的基础。但很惋惜的是,我并无完成这个任务,由于在此次做业快要截止的时候,我发现本身的程序没法很好地处理同时有不少个请求一块儿输入的状况。这个问题也在互测中被测个人大佬一下就挑了出来。究其缘由,是由于我为每个请求都开启了一个线程,并让其运行三秒钟后自行终止,这虽然很是符合真实的逻辑,但却不适用于程序自己。由于每个请求线程均可能会改变出租车的状态,所以须要为这个请求线程中涉及到变动出租车状态的地方加锁,一旦请求变多,达到百条的量级,就会使得线程之间互相阻塞,后面的请求得不到执行。此外,因为用户能够自由输入请求,因此实际上程序的线程数是由用户控制的,这显然是一种极不安全也极不合理的设计。在进行下一次出租车做业以前,我会想办法解决这个问题,把线程数控制在一个本身可控的范围内。
本次做业的经典OO度量以下:
此次做业的代码量并不大,1473行是包含了GUI的统计,将GUI排除在外后,实际只有900行不到。但我仍然以为程序在许多地方显得过于臃肿,请求队列类几乎形同虚设,出租车线程设计得也不够优雅。这些须要在重构的时候加以解决。
本次做业的类图以下:
在类设计中,几乎全部的数据操做都是围绕TaxiSet类展开的。TaxiSet包含了全部出租车的信息,请求线程只能访问到TaxiSet类,而不能直接对Taxi进行操做。这使得多个请求线程可使用synchronized以保证不会出现数据冲突的状况。
本次做业的时序图以下:
TaxiDispatcher出租车派遣类是整个程序执行流程的核心。TaxiDispatcher就是我所说的只会运行3秒钟的线程,它会从请求队列中提取请求,并通知乘客出发点周围的出租车抢单,并最终决定调度哪一辆出租车为乘客服务。
个人程序在多线程电梯和出租车调度中各被报告了一个Bug,其中多线程电梯是因为忘记对某一块输入部分进行处理而致使的公测格式错误,出租车调度则是上文中提到的没法同时处理大量请求的错误。前者是因为粗心马虎和测试不周全而致使的Bug,后者则纯粹是由设计致使。值得注意的是出租车调度的Bug,它使我对程序内线程数量和程序性能的关系有了更深的理解。
多线程电梯的互测中,我找到别人的Bug主要集中在捎带的判断上。多是因为模拟时间和真实时间的同步没有作好,有些应该判断为捎带的地方对方并无判断成功。我想这种问题很难从代码层面直接挑出来,只有经过大量样例的测试才能发现。文件监视器的互测中,我主要经过阅读别人的代码发现了Bug。对方没有作好重命名时的多映射检测,也没有完成指导书中要求的继续监控移动后文件的任务,这些Bug均可以在仔细阅读代码之后直接找到。更深层次的缘由是我在写程序的时候也遇到了这些问题,所以在互测的时候就会对它们格外关注。出租车调度的互测中,因为代码量较大,且直接从代码中找逻辑Bug相对困难,我采用了集中压力测试的方法,即一开始就让全部的出租车集中在地图的左上角,而后集中输入请求进行压力测试。经过这样的方法,一些隐蔽的Bug才能被发现。
多线程程序的代码逻辑相比单线程程序复杂不少,有时候直接阅读代码也难以找到其中的漏洞。这个时候,测试样例的广度覆盖和压力测试的深度覆盖就显得颇有必要了。此外,找到别人Bug的另外一个好方法是回顾本身的设计过程,细数本身在写代码的时候踩过哪些坑,而后再去看别人是否犯了相同的错误。
不少同窗都将多线程称之为"玄学",我想这是有必定道理的。不一样于单线程程序的彻底可控,多线程程序在运行的过程当中可能会出现许多难以预料的行为,甚至有些行为不可复现,但对程序却有着致命的影响。编程者该作的,不该该是想着如何回避甚至掩盖这些问题,而是应该努力地去暴露问题,并争取对其加以修复。
提升多线程程序的性能并不困难,保证多线程程序的线程安全也不困难,但要想同时作好这两点,就变得很是困难。在这三次的做业中,我遇到的几乎全部多线程问题均可以归根结底为一句话:如何在性能和安全之间作出取舍。程序的时间须要和真实时间保持一致,这是对性能的要求,然而为了兼顾多线程的安全性,编程者可能须要采起一些同步控制的方法,这其中的时间差势必会致使程序时间和真实时间的不一样步。这三次做业中,我尝试了一些解决这个问题的方法,最终发现,将模拟时间和真实时间结合起来,先计算出程序应该运行的时间,而后再让它睡到那个时间,这种方式既省脑子,也省资源,还能确保程序运行的正确性。
除此以外,程序的架构须要有一个足够优秀的设计才能经得起需求变动的考验。在写代码以前,先花一两天的时间在纸上写写画画,大体勾勒出程序的框图;而后先不写方法的主体定义,只写方法名和返回值,用这些尚待完善的半成品方法和类的属性搭出一个程序;最后,为每一个方法填上具体的内容,完成整个程序。这一套流程能够有效地检验程序设计是否合理,也必定程度上减轻了工做量。我在出租车调度做业中使用了这个方法,并取得了令我满意的成果。
多线程编程仍是颇有意思的,当看到出租车在GUI上动起来的那一刻,个人心中真的有一种巨大的成就感。但愿接下来的三次做业也能像前两个单元同样顺利。