面向对象第二单元总结
面向对象第二单元总结
第二单元的三次作业需要完成的任务分别为:单部电梯的一般运行,多部电梯的协同工作,多部多种类电梯的协同工作,下面为本单元三次作业的总结和分析。
一、同步块设置与锁的选择分析
第一次作业
在第一次作业中,我选择的同步块在输入线程InputThread
和电梯运行线程Work
中,选择的锁只有一个——整个电梯程序中的WaitQueue
这一等待队列对象。在这次作业当中锁与同步块的关系是锁即是同步块的执行资格,每个同步块都必须且仅须得到唯一的一个锁即能拥有运行的资格。由于我没有采用对方法使用synchronize的方式所以同步块在实现上和锁其实没有区别。
第二次作业
到了第二次作业,加入了多部电梯的要求,这时如果还是仅仅只有一个等待队列对象不足以支持多部电梯的并发,所以我把锁的对象分为了两部分,一部分就是PublicQueue
代表的总的公共等待队列,该对象仅提供输入线程InputThread
和调度器Scheduler
使用。另一部分则是对于每部电梯的运行线程都分配一个自身的WaitQueue
对象来管理所有需要且仅能由该部电梯去处理的请求。
如此一来,同步块就分为了三部分,分别是输入线程、调度器和电梯运行线程。而锁的选择则是PublicQueue
和每部电梯的WaitQueue
。在输入线程块中锁的对象是PublicQueue
,输入线程把需求记录到该对象中去,以供调度器之后的调度;在调度器中锁的对象则是二者都有,首先锁住PublicQueue
从其中读取待处理的请求,然后在这个锁的内部再锁住将要分配的电梯的WaitQueue
对象,将请求写入到其中,同时把它从公共等待队列中删除表示已经分配了电梯来运行这一请求了;电梯运行线程则只锁住自己的WaitQueue
对象,读取其中的请求以控制电梯的运行,同时在请求被正确处理之后将其从队列中删除。
第三次作业
第三次作业由于我并没有找到太好的关于换乘的策略所以并没有将换乘策略加入到我的电梯运行中去,所以第三次作业只是在调度器上进行了修改,使得修改能够额外以电梯类型和请求的匹配情况为要素尽心请求的分配。所以在同步块的设置和锁的选择上和第二次作业没有什么太大区别。
二、调度器的设计分析
由于三次作业中,第一次作业没有设置调度器,第二、三次作业调度器差别很小所以就简化为以添加调度器的第二次作业为分析对象进行分析。
我的调度器设计上思路是先拿到待调度队列PublicQueue
,然后在调度时采取一定的策略分配给指定电梯的待处理请求队列WaitQueue
。同时在这个过程中对两个队列都要进行增删请求的操作。概括来说就是从PublicQueue
中拿请求出来,在放到一个合适的WaitQueue
中去。
调度器在程序中的作用其实是串联起了两个线程之间的关系,即输入线程和电梯运行线程,使得两个线程能够正确地协同工作。所以说调度器不仅仅是在调度请求,更是沟通者的身份。
三、第三次作业的可扩展性
功能设计
在功能设计上,我的程序扩展性大致是勉强能扩展的。功能扩展性主要考虑的应该就是在电梯调度和电梯运行的功能上进行添加。而我所有的电梯运行功能由两部分组成,一部分是电梯基础功能,这些集成在了Work
父类中;另一部分是不同工作模式下的调度策略,而调度策略一共由两方面决定,一方面是调度器的分配策略,另一方面是电梯运行本身的运行策略。这一部分要增加功能需要对调度策略进行更改。改动幅度与策略改动幅度成正比。
性能设计
性能设计上第三次作业本身由于没有找到好的调度策略,所以没有加入换乘机制。同时本身的调度策略上性能也不够优秀,所以在性能上扩展性很差。
四、分析自己程序的BUG
- 线程有时不能正常结束。在我的
Morning
模式电梯中,如果在输入数据中出现了两个请求输入时间差距比较旧,接近于两秒左右的情况下,我的程序会由于在最后的请求输入已经完成,且发生了最后一次notify
的情况下,再次进入wait
状态,导致无法再被唤醒,从而形成死锁。 - Random运行类型的策略选择逻辑不正确。我的
Random
模式电梯会在请求稍微复杂一点的情况下就出现超时的问题。究其原因发现是在选择电梯运行策略时,过于简单,主要是从队列的角度去考虑的,而没有在整栋楼的运行实际方面上去考虑。结果就是只要每条请求遇上一条请求之间运行方向相反,起始楼层相差巨大就会使得电梯忙于奔命,跑最远的距离装载最少的乘客。
五、BUG分析策略
在BUG分析上,我主要从调度的合理性和电梯运行的正确性上去考虑测试。
调度的合理性
- 考虑每部电梯在运行的时候是否采取了合适的调度策略,而不是过于简单的调度策略,例如过于随意的捎带等类似策略。这里先测试单部电梯运行的合理性。
- 考虑调度器的分配策略是否合理。给出一些比较集中于一种类型的请求,和按一定比例组合混合请求,以此来测试调度器的分配是否过于简单,或者存在不合理的调度使得大量的请求集中于单部电梯上而其他电梯本来能够做一些运输但却大多数时间处于空闲状态,这些都是不合理的。
电梯运行的正确性
- 考虑电梯在运行的时候是否能在正确的楼层停留,乘客上下电梯的情况是否正确,以及在运行过程中是否存在超载现象的产生。
两个单元测试策略的差异
最主要的差异还是在于本单元的多线程是与时间有关系的,每次测试在不同的时间输入相同的数据都是有区别的。而由于本单元的电梯运行单纯在行为上其实并不难以控制,真正难以控制的是电梯在不同模式下的运行效率和性能。所以在测试上,主要测试在三个运行模式下,在不同时刻输入有明确测试目的的请求,充分调动电梯,同时也充分体现出时序性,针对可能存在的死锁问题进行测试。
六、心得体会
线程安全
- 凡是涉及共享对象的都先加锁。只要是涉及到对共享对象的操作——无论是读还是写——都一定要先予以加锁,因为多线程最关键的就是一个线程的任意两条语句之间都可能执行了另外一个线程的任意多条语句。这不能说是心得体会,应当说是多线程中最最基础,一定要做到的一点。
- 记得唤醒所有wait的线程。在三次作业中我不止一次地遇到程序无法正常结束的问题,最后发现我没有因为加锁对象的不正确而导致的死锁,而是因为有的线程有时在wait 之后再没有线程来唤醒它了,导致程序一直等待没有正确结束。另一个不能正确结束线程的原因多半是因为在同步块的while-true循环当中没有正确考虑结束条件,导致不该wait 的时候去wait了,而在唤醒的时候又没有针这种情况考虑进行唤醒。
层次化设计
本次作业自我感觉在层次化设计上对比第一单元有了不小的进步,虽然主要得益于实验课。其实多线程本身就要求设计不能太过随意,否则很难使程序正常工作。这一单元作业知道要考虑使用继承、方法覆写等方式去管理一个类了,所以我在电梯运行上就采用了这一方式。这主要体现在有三种电梯运行模式需要分别服务,所以在除了调度策略方面,其他很多共性功能,如向上向下运行,开门关门,装载乘客,卸载乘客等都在一个电梯运行的父类Work
里面分别实现了。所以到了子类的继承中只需要对于调度策略做出决策即可。这样的设计可以降低代码的重复程度,提高代码的复用,同时这也确实提高了代码的可读性。代码不再冗杂就可以方便理清代码逻辑。另外就是在同步块的设置上分为了输入、调度和运行三个线程在运行,彼此耦合并不强烈,也比较方便增加功能。