OO第二单元总结

0.总述

第二单元的任务是多线程电梯的调度,旨在完成多线程的并发控制以及之间的协作。三次作业依然是由易入繁:

第五次作业:单电梯,无捎带。

第六次作业:单电梯,需捎带。

第七次作业:多电梯,电梯之间需要协作。

在写这个单元的作业时,我吸取了上一个单元的教训,仔细思考了程序的结构,使得代码易于扩充,避免像上个单元一样大量的重构代码。

以下是我对这一单元作业的总结。

1.第五次作业

1.1设计策略

此次作业只有一部电梯,而且不需要捎带,因此较为简单。

我的设计为三个对象:一个调度器负责读入数据并且把数据存入到等待队列中,一个等待队列存储调度器收到的请求,一个电梯从等待队列中获得请求并完成请求。两个线程:调度器线程和电梯线程,共享一个等待队列对象。调度器线程的任务是不断的从输入中获得请求,并把请求存储到共享队列中,当请求输入完成后,改变共享对列中的一个标志为false表明没有更多的请求了。电梯线程是当电梯内为空时,查询共享对列中的第一个请求,移动电梯去接该请求;当电梯内不为空(因为是无捎带电梯,此时电梯内只有一个人),就移动电梯送该请求。电梯线程的终止条件为电梯内无人,共享队列中无人,共享队列的标志为false(没有更多的请求了)。

对于同步控制,电梯和调度器共享了一个等待队列,电梯会读写该队列,调度器会写该队列,所以此次作业中简单地把等待队列中的方法前加synchronized中即可(事实上,此次作业中我为了确保多线程运行过程中不出现差错,在所有的方法前都加了同步控制锁,现在看来是多此一举的,并且会影响运行的效率)。

对于线程间的协作,此次作业中只有两个线程,因此该问题就是何时进行线程切换的问题。我采用的策略是,当电梯无人可接送时立刻通过yield()方法切换至调度器线程,当调度器读取到一个请求时,立刻通过yield()方法切换至电梯线程。此次代码中我未采用wait()和notifyAll()方法,因此,虽然可以正确完成此次作业,但CPU的占用时间较多。

1.2度量分析

方法统计:

复杂度分析:

代码行数:

 

此次作业的难度比较低,所以复杂度比较低,每个类的代码行数也比较少。

类图:

 

可以看出,主函数生成了电梯线程和调度器线程,这两个线程通过等待队列联系起来,此次代码类的耦合度较低,有较好的分离。

协作图:

 

这一次作业的协作比较简单,按顺序把读入的请求一个一个地送达即可。

1.3 bug分析

此次作业提交的最终版本没有发现bug,但在提交的过程中发现了两个bug:1.线程无法停止。解决方法是加入了一个tag标志用于控制。2.同步控制有问题导致输出出现错误。解决方法是在应有的地方加同步控制锁。总的来说这次作业不是很难,主要是学习如何写多线程代码和理解锁的作用。

2.第六次作业

2.1设计策略

此次作业较上次作业增加了捎带要求和负的楼层,整体架构和第五次作业的相似,在电梯线程中增加了调度策略。此次作业采用的调度策略为指导书提示的调度策略:ALS(可捎带电梯),满足的条件如下 :

可捎带电梯调度器将会新增主请求和被捎带请求两个概念

主请求选择规则:如果电梯中没有乘客,将请求队列中到达时间最早的请求作为主请求;如果电梯中有乘客,将其中到达时间最早的乘客请求作为主请求

被捎带请求选择规则:电梯的主请求存在,即主请求到该请求进入电梯时尚未完成,该请求到达请求队列的时间小于等于电梯到达该请求出发楼层关门的截止时间;电梯的运行方向和该请求的目标方向一致。即电梯主请求的目标楼层和被捎带请求的目标楼层,两者在当前楼层的同一侧。

其他:标准ALS电梯不会连续开关门。即开门关门一次之后,如果请求队列中还有请求,不能立即再执行开关门操作,会先执行请求。

调度器线程和第五次作业的行为一样。电梯线程是当电梯内为空时,查询共享对列中的第一个请求,把该请求作为主请求,移动电梯去接该请求;当电梯内不为空,电梯向主请求的目的楼层移动,沿路接送满足被捎带条件的请求。在主请求送达后,若电梯内仍有请求,则把第一个请求作为主请求,执行当电梯内不为空的策略;若电梯内无请求,执行电梯内为空时的策略。

在设计之初,想要使用老师上课讲的观察者模式,于是写了观察者和被观察者两个接口,让调度器和电梯类分别实现这两个接口,但之后觉得这样写与我之前的架构相性不好,所以代码中虽然留有这两个接口,但此次的设计模式算不上观察者模式。

同步控制上,采用和第五次作业相同的方法,即对共享对象提供的方法加同步控制锁。

线程间的协作上,此次作业我已经意识到使用yield()方法可能会占用大量的CPU资源,而wait()和notifyAll()方法更适合用于多线程的并发控制当中。于是我在两个线程内都传入一个锁,当电梯无人可带(电梯内和等待队列里都没人)时,就会在这个锁上调用wait()方法,而调度器每次收到一个请求就会在这个锁上调用notifyAll()方法,确保在有请求未处理时电梯线程能够执行。

2.2度量分析

方法统计:

复杂度分析:

 

可以看出,有三个电梯接送人的方法复杂度较高,这些方法里有很多的if语句,没有把它们拆分成更小的方法是我在此次作业中做的不好的地方。

 代码行数:

 

由于接送人的方法都写在了电梯类中,所以电梯类的代码行数较其他类明显的多。

类图:

 

此次代码的架构与上次作业相比,除了多了两个接口外是一样的。电梯和调度器通过等待队列联系起来而二者是分离的。

协作图:

 

这次作业和上次相比只增加了可捎带选项,因此线程的协作方面增加了电梯每到一层,检查是否有请求要出入

2.3 bug分析

此次作业提交的最终版本在公测中也没有发现bug,但是性能很低,强测中有八个样例的性能分都是0分。所以下次作业时,我除了思考结构框架外,也较多思考了电梯的调度策略。

3.第七次作业

3.1设计策略

此次作业较上次作业,从一部电梯增加到三部电梯,并且三部电梯所能够停靠的楼层也有限制,这就不仅要求多部电梯线程间的并发控制,而且电梯之间要有协作。

同步控制上,有两个地方需要:1.等待队列。这次不能像之前的两次那样,在需要读写等待队列时,把整个对象加锁,使别的线程无法读写。因为这里有三部电梯,当一部电梯在某一层接送人,不应该影响另一部电梯在别的层接送人。因此我用了一个数组,数组的每一个元素是一个队列,每一个队列分别有一个锁,这样减小了锁的范围,使得不同电梯能够在不同的层同时接送人。2.输出。因为输出接口不是线程安全的,所以在某一个输出进行时要加锁,这个比较易于实现。

调度策略方面,我最初的设想是调度器每收到一个请求,就计划好这个请求换乘与否和换乘方法,然后在规划好的电梯来到请求对应的楼层时把请求加入对应电梯。但这样很可能出现的后果是,一个请求苦苦等待它想要的电梯,而虽然不能带它到目的楼层但可以让它离目的楼层更近的电梯来时却不上去,这样就会增加运行时间,而且不能充分地利用电梯之间的协作。所以我想,既然一部电梯能否到某一层是一个电梯对象的固有属性,那一部电梯能否接从哪一层到哪一层的请求,应该把接到的请求送到哪一层也应该是一个电梯对象的固有属性。因此,我设计的每一部电梯可以接的请求和把请求送达的目的楼层如下。

A电梯:可以接-3, -2, -1, 1, 15-20(即能停靠的楼层)的请求;对于要到2-14层的请求,把它送到1或15层(取决于先到哪一层),其他楼层直达。

B电梯:可以接-2, -1, 1, 2, 4-15(即能停靠的楼层)的请求;对于要到-3的请求,送到-2层,要到3层的请求,送到1或5层,要到16-20层的请求,送到15层,其他楼层直达。

C电梯:可以接1, 5, 7, 9, 11, 13, 15但目的楼层不是1-15偶数层的请求和3层的请求;对于要到-3—1的请求,送到1层,对于要到16-20层的请求,送到15层,对于要到1-15偶数层(这个请求一定是来自3层)的请求,送到1或5层,其他楼层直达。

这样每部电梯在到某一层时,按上述要求检查是否要接送人。

电梯整体的调度策略采用扫描算法,即一直朝一个方向运行直到这个方向上没有要接送的人后换方向。

线程间的协作上。在电梯没有人可以接送时电梯要进入睡眠,当调度器得到新的请求或者别的电梯出来的人未到达目的层(因为有的请求必须倒电梯)时,就要唤醒所有的电梯来检查新的请求是否能够接送。最后,当没有更多请求时调度器线程结束,在等待队列为空,且三部电梯内都为空时,三部电梯才可以结束线程。

2.2度量分析

方法统计:

复杂度分析:

 

 可以看出,和上一次作业一样,有关接送人的方法的复杂度较高。

代码行数:

 

 总的来看调度器类和电梯类的行数还算均衡。但我这次把等待队列和调度器写在一起了,现在看来这是极为不符合单一职责原则的行为,我以后写程序会多注意架构的设计。

类图:

 同代码行数暴露的问题一样,我把等待队列和调度器放在同一个类内的行为违反了设计原则,导致类十分臃肿,类与类之间的联系也变多了,耦合度增加不利于代码的维护。我现在非常深刻地意识到了这个题,以后设计前要多想想是否遵从设计原则。

协作图:

 

从协作图上来看,完全是两个类间的协作,虽然能够正确完成任务,但这两个类间有太多的联系,的确很不合理,此次作业我应该再自己思考一下,如何在现有代码的基础上重构使得程序架构更符合设计原则。

2.3 bug分析

此次作业提交的最终版本在公测中也没有发现bug,我在写代码的过程中遇到的bug也都较易修改,在有前两次多线程作业的经验后,在并发控制方面写起来也较为得心应手。这次代码麻烦的地方在于多个电梯的协作上,并且还要尽可能的提高效率,强测中我15分性能分得了7.6分,性能还有很大的改善余地。

4.心得体会

4.1线程安全

线程会出现不安全情况的原因是多个线程并发访问一个共享对象,由于线程间的竞争会出现难以预料的问题,比如第七次作业中,如果不对某一层的等待队列加锁,就会出现两个电梯把同一个请求接到自己电梯内的情况,这显然是错的。为了保证线程安全,就需要正确的使用锁。这几次作业中使用锁的方式有在方法前面加锁和用锁同步一块代码两种。在方法前面加锁主要用于共享对象中会改变对象状态的方法中,把共享对象设计为线程安全类可以尽可能减少同步范围,也可以让使用共享对象的类无需采取针对共享对象的同步控制措施;对于线程中某一块代码需要进行同步控制,就需要对这一块代码加锁。值得注意的是,要注意加锁代码的范围以及为尽可能减小同步范围而适当调整代码顺序,比如把sleep(100)加入到同步控制的代码中虽然不会错,但获得锁的线程再sleep()的过程中不会释放锁会导致别的线程在执行到这段代码时会被迫多等待100ms.

锁的本质是对象,因此要想一个锁在多个线程中发挥作用,就要保证这些线程中的锁是同一个对象。比如在调度器和多个电梯线程的协作上,电梯的睡眠和唤醒都是要在用一个锁上,所以我就new一个对象专门用作锁传入它们的构造函数中。

4.2设计原则

这几次作业我对一些设计原则又有了些新的体会,一个好的设计更易于开发易于维护。

遵循单一职责原则:一个类只专注于做一件事,高内聚,低耦合;对于这两个原则在第五次和第六次比较简单的情况下我做得还好,调度器收到请求放入共享队列中,电梯调用方法获得请求完成接送。但在第七次作业中我明显感觉到我的调度器和电梯间的联系有些多了,电梯线程调用调度器的方法获得对象,而这个方法又需要调用电梯的方法来做判断。我觉得我再重构一下可以把它们的职责分得更开一下。

最少知识原则: 对象与对象之间应该使用尽可能少的方法来关联,避免千丝万缕的关系;低耦合;类知道其他类应尽量少;类可以访问其他类的方法或者属性也应尽量少;前两次作业当中我的调度器和电梯完全分离,倚靠一个等待队列的类联系。但在第七次作业中我出于更方便写的目的,把等待队列作为一个成员变量加入到调度器的类中,这使得调度器和电梯的联系增加了很多,现在看来把等待队列单独作为一个类会更好一些。

同步控制:同步控制的范围应该尽可能的小,这样才能够提高运行的效率。我在这几次作业中也是逐渐意识到同步范围大小的重要性,在第七次作业中认真思考了哪些代码应该做同步控制,尽可能的缩小了同步控制范围。

方法统计:

转载于:https://www.cnblogs.com/Cats-on-Mars/p/10756145.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值