面向对象第二单元总结与心得体会

大体思路:

第二单元的作业要求我们实现能将乘客搭载到不同楼层的电梯。

一开始我的设想是完全模拟现实生活的情况,即每个人、每部电梯都设一个线程,电梯采取ALS策略,而人知道电梯的ALS策略,且会作出对自己最有利的选择,让这些线程交互从而达到最优的效果。但是很快我就放弃了这个想法,一来这种想法实现起来很困难,线程繁多,实现起来复杂,而且一不小心就会出现死锁一类的问题,二来考虑到扩展电梯后乘客无法判断电梯的内部情况,这种精明的乘客将变得毫无意义。因此我改换思路,只保留电梯线程,采用生产者-消费者模式,将乘客物化,把乘客的主动要求变成了不可更改的属性。

第一次作业

作业初步分析

以下为第一次作业的UML图

 面向对象第二单元总结与心得体会_第1张图片

 

 

 

 

在这次作业中,各个类分工明确,主类负责初始化及启动线程(其中私有方法getSleepTime是我自己方便调试而加的),生产者负责产生数据并提醒电梯,托盘负责管理控制数据,电梯将乘客送达目的地。

此次作业设计的关键在于电梯。电梯中,run方法只有几行代码:

 

面向对象第二单元总结与心得体会_第2张图片

 

 

这控制了电梯的行为。电梯每次循环都上/下/停一层,循环开始便等待指令,如果有没有任何指令则进入休眠状态,直至外部对象将其唤醒,而后上下乘客,先下后上,最后再依据电梯内外的指令情况改变自身状态。电梯退出循环的依据是:接收到了生产者发来的信号(没有新的乘客了),以及电梯内外都没有指令了。这和等待指令的条件只相差来自生产者的信号。

 

此次电梯作业,设计思路清晰,但是不易扩展。原因在于抽象程度不够,这也是我忽视的一点。第一单元作业过后,我并没有认真思考如何提高程序的可扩展性,结果就是抽象性不够,各种类之间直接连接在一起而非通过接口,这就大大提升了耦合度,尽管在三次作业中并没有造成非常大的麻烦。

 

 面向对象第二单元总结与心得体会_第3张图片

 

 

 

 以上是Metrics分析结果,这些都是电梯run方法分出去的子方法,本身和run方法紧密相关,其他对象无法调用,非常特殊,也就没有耦合性的考虑。不过,ev过高可能说明这些方法难以维护和debug,也就说明这些方法可以再次细分,得到更为清晰的逻辑(当然会使得方法间调用关系更为复杂,但是细分带来的逻辑清晰化可以克服方法调用关系复杂化的缺点)

 

线程安全分析

本次作业加锁的对象只有两类:电梯和托盘。电梯加锁的原因是电梯会主动等待指令,而外部有唤醒电梯的可能。如果不加锁就可能出现唤醒之后陷入沉睡的情况,在输入接近结束时可能会导致bug;托盘加锁则是为了保证访问数据的安全,为了防止出现紊乱,对put、get和empty查询三个方法加锁。但是这中间出现了一个死锁:电梯要进入休眠,就必须查询托盘是否为空,假设此时电梯已进入等待检查方法但未检查,而生产者正在放数据(put方法最后唤醒电梯),这就出现了死锁:put要唤醒电梯,就要电梯退出等待检查以获取锁,而电梯要查询托盘状态就要生产者结束put释放锁。但是可能是由于单部电梯产生死锁的几率较小,加之同房间互测不活跃,导致bug没能发现,也就直接导致第二次作业的惨淡。

BUG分析和DEBUG策略

我所采用的BUG分析策略依然是静态检查(或者说小黄鸭检查),但是由于对于死锁认识不充分,加上死锁的隐蔽性很高,自己 又没有系统检查死锁的方法,导致了bug的出现。

 

DEBUG策略没有,因为那周有别的事需要做。不过现在想来,这种简单电梯可能真的没有什么可以发现bug的地方,线程不安全,就基本上没有共同的了。

第二次作业

作业初步分析

 

 

面向对象第二单元总结与心得体会_第4张图片

 

 

 

 

这次作业相比于上一次作业没有什么大的不同,只是加了几部电梯、改了停靠楼层而已。因此所有架构沿袭上一次作业(当然也把致命bug沿袭了下来),当然也为下一次作业做了一些铺垫和优化:

·加入了可停靠楼层的概念。主要是针对下一次作业而加入的设计,配套加入了楼层和下标相互转换的方法,并将这种模式也在托盘中复制了一份。

·进一步细分方法。run方法中加入了arrive的方法将原本放在其他地方的arrive语句封起来,并优化了循环流程使之更好理解:到达(上行或下行时输出)->等待指令(有指令就直接跳过,或者被唤醒)->乘客流动(此时之前设计的先下后上就起作用了)->电梯状态改换(上楼或下楼或停下)。同时将所有开门、关门的动作也分离了。

·改动了托盘的get和check,考虑到有人数上限,这两个方法只能一个个取指令了(也可以说,大批量取指令是上一次作业考虑不周)

面向对象第二单元总结与心得体会_第5张图片

 

 以上是第二次作业的Metrics分析,可以看到电梯中主要是状态改变的方法,而托盘也因为if过多而ev过高,总体问题不大,不过if过多确实会导致debug困难一些

线程安全分析

因为沿袭了上一次作业,所以死锁也带到了这一次。同时还出现了另外一个意想不到的bug:电梯吃人。电梯在满载的时候依然会从托盘取人,但是忘记放回去了,这就导致了问题。

 

以上都是解决了的问题,但是在解决过程中确实发生了一些有趣的事。因为多线程的不确定性,我决定利用测评机debug,但是当我加入了大量输出语句想要确定bug来源时,原本大片的rtle都变成了wa,即使我将这些输出全部换成了空打印,rtle依然很少出现。照理说原本线程不安全的一定还是不安全,但是加入输出语句大量rtle就消失确实很奇怪,而且这种原因也不可追踪。此外,当我去除所有不必要的输出语句并修复了电梯吃人的bug后,rtle也没有大面积卷土重来。我只能认为这是测评机的玄学机制。

BUG分析和DEBUG策略

我引以为豪的静态debug法失效了。其实这种失效也很好理解,这种方法的基础是绝对的仔细和对知识的绝对掌握,然而当时的我并不具备这种条件。至于电梯吃人则是我不应该犯的错误,这也许可以归因为自大,因为当时我仔细检查了所有代码,却并没有测试电梯满员的情况。debug的方法其实也很简单:各个击破。已知所有的bug中出现了rtle和wa,但是没有ctle,说明不存在互相调用陷入的死锁,也就是说,死锁和synchronized修饰的方法有关;同时存在和死锁并无关系的wa,说明其他地方出了问题。最终,我将wa的测试点放在本地测试,发现了电梯吃人的bug;通过回忆死锁发生情况和仔细查看代码最终确定了死锁位置,把唤醒电梯的过程从put中分离出去, 交给调用put方法的对象自己唤醒。

心得体会

主要是死锁的心得体会。死锁一般而言分为两种,一种是循环调用,一种是相互等待。循环调用产生的后果是cpu超时,互相等待的后果是rtle,而这两种情况的产生都离不开相互调用。因此在相互调用时一定要加倍小心,避免出现死锁的情况。此外,如果多个锁对象出现循环调用,也有可能出现死锁。

第三次作业

作业初步分析

 面向对象第二单元总结与心得体会_第6张图片

 

 

 

因为第二次作业埋了基础,因此第三次作业也并没有大的架构上的改动,但是有几点是值得注意的:

·输出的线程可能存在不安全的问题。实际上我也不知道课程组给的timableoutput是否安全,不过保险起见,我还是给它加了把锁。不过由于这把锁过于简单,能传给timableoutput的参数只能是字符串,好在java的机制确保任何形式都可以转化为字符串。

·乘客需要换乘。本次作业的乘客需要换乘,因此需要为乘客规划路线。在这里,我只是简单考虑乘客的策略,即乘客只知道电梯的可停靠楼层而忽略各电梯所在楼层所做出的换乘策略。这种情况下乘客会做出最利己的决策,即能直达就不换乘,可搭乘多部电梯时选择停靠楼层较少的楼梯。

·线程结束的标志。此时不能仅仅通过判断无新乘客和无指令来判断了,因为有可能最后一名乘客需要换乘,但是他正在电梯中,此时他的换乘电梯认为没有任务就结束自己,这样这名乘客就无法到达了。因此我的判断是各个电梯的状态。当各个电梯都处于休眠状态时,监督员认为任务可能结束了,并继续观察,在1秒1次,40次观察到相同的结果后,即认为任务结束,唤醒所有电梯并发送信号,结束所有线程。之所以观察这么多次,是为了防止出现一次不准确的情况。因为线程安全的缘故,可能会出现所有电梯都在休眠而实际上任务并未结束的情况,每观察一次,错判的概率就增加幂,一次错判概率的40次幂基本为0.当然了,现在看来这种方法过于愚蠢:一旦碰上大量数据,这种方法露馅的概率依然很高;而且有一种更好的方法:计算系统中总人数,初始为0,生产者每put一次+1,电梯每放出一个到达的乘客-1.不过,这其中的妥协的艺术还是比较值得玩味的。

·类的集合。设立一个电梯类,其中储存了各类电梯的各种信息,并通过static方法将这些信息释放。这在某种意义上可以算作一个简单的工厂,所有的依赖电梯类的定义的部分都可以在这个类中查询。当然可以一类电梯设置一种类,不过这样的话其他类想要获得电梯的信息恐怕就要instanceof了(也就是说我的电梯类可以算作是一种抽象类,而具体的实现都在elevatortype中了)。我认为这次作业各类电梯的分野介于能独立设类和不需要独立设类之间,我本人写的时候也纠结了一番。

 

以下为此次作业的度量:

面向对象第二单元总结与心得体会_第7张图片

 

 

 依然是ev大了。当然这也难以避免,电梯和托盘前已分析,生产者则是因为分饰两角,而电梯类则是因为需要依据给定类不同给出不同参数,if很多所致。

BUG分析和DEBUG策略

本次的bug分析和debug依旧沿袭了之前的策略,但是加入了一些trick:使用前一次的bug修复。其实这也是本单元的特色:所有的指令都是关于乘客的,而无关信息规格化输入会帮我们滤掉,自然参数设置得当就能直接利用前一次的bug修复帮助debug。

 

BUG分析和DEBUG总结

在本单元作业中,静态debug方法遭遇了一次失灵。原因有二:一是静态debug不能单孓孤立,必须辅以必要的边界测试来确保准确性,二是必须对可能出现的bug有着非常深刻的理解和非常丰富的经验。而这两点在第二次作业中都没有完全具备,因此遭遇了失灵,当然也对我的自信心造成了非常大的打击。

 

心得体会总结

我再也不想碰多线程了,我讨厌不确定,我讨厌各种玄学。

当然收获还是很大的,除了深入了解多线程之外还了解了避免死锁的方法,同学们的分享也非常有价值。

你可能感兴趣的:(面向对象第二单元总结与心得体会)