面向对象第二单元总结 - 之 - 吾梯永不停 - 简体版本

面向对象第二单元总结 - 之 - 吾梯永不停 - 简体版本

目录
  • 面向对象第二单元总结 - 之 - 吾梯永不停 - 简体版本
    • 一、设计策略
      • 1.1 调度策略
      • 1.2 多线程协同与同步控制
      • 1.3 电梯进程与请求队列接口概述
    • 二、SOLID原则 - 之于 - 作业3
      • 2.1 SRP (Single Responsibility Priciple)
      • 2.2 OCP (Open Close Principle)
      • 2.3 LSP (Liskov Substitution Principle)
      • 2.4 ISP (Interface Segregation Principle)
      • 2.5 DIP (Dependency Inversion Principle)
    • 三、程序结构分析
      • 作业1
      • 作业2
      • 作业3
    • 四、bug分析
    • 五、发现他人bug策略
    • 六、心得体会
      • 6.1 线程安全
      • 6.2 设计原则
    • 七、写在最后


一、设计策略

1.1 调度策略

三次作业我都采用了同样的电梯调度算法——LOOK算法。什么是LOOK算法呢?这要先从SCAN算法说起。

  • SCAN算法

SCAN算法是一种按照楼层顺序一次服务请求的算法。它让电梯在最底层和最顶层之间连续往返运行,在运行过程中相应处在电梯运行方向相同的各楼层上的请求。SCAN算法的平均响应时间比SSTF算法长,但是响应时间方差比SSTF算法小。从统计学角度来讲,SCAN算法要比SSTF算法稳定。

如果以指导书上的标准算法ALS的角度来看的话,其实可以将SCAN算法理解为:在电梯中永远都有一个虚拟的主请求,即电梯上行的时候主请求就是一个从最底层到最顶层的虚拟请求、电梯下行的时候主请求就是一个从最顶层到最底层的虚拟请求。此时一切客观存在的真实请求便都是这个虚拟的主请求的可捎带请求。

  • LOOK算法

LOOK算法是对SCAN算法的一种改进。对LOOK算法而言,电梯同样在最底层和最顶层之间运行,但当电梯发现当前电梯所移动的方向上不再有请求时(电梯内没有到达请求,电梯外没有呼梯请求),电梯会立即改变运行方向。而SCAN算法会继续运行到最底层或最顶层才改变运行方向。

使用LOOK算法的一个原因就是感觉这样的调度算法写起来较为简单:只需要写出电梯的反向条件和开门条件,让电梯自行去接人,这样就是一个比较完整的调度算法,而不需要考虑设置调度器为电梯分配请求。

使用LOOK算法的另一个原因就是因为LOOK算法对捎带及其友好。LOOK算法对所有请求一视同仁,不会将请求分为奇奇怪怪的主请求和捎带请求。只要电梯容量允许,就可以捎带任何的请求。此时,捎带的条件就及其显然了。

1.2 多线程协同与同步控制

在本单元作业中,我使用了类似于生产者消费者的模式,但也根据具体需要对其进行了一点调整。在我的程序中有两类线程:主线程——负责输入、电梯线程——负责实现请求。我的程序中还有这两类线程互相交互的托盘类:请求队列、结束标识等。如果有多部电梯的话,多部电梯之间没有交互,彼此透明。

对于多部电梯的情况,我认为设计一个调度器本身就不符合LOOK算法的思想。LOOK电梯的思想是:电梯自行决定运行方向,并做到能接人则接人。因此无需对电梯施加过多的影响,无为而治即可。仅仅在作业2、可能有多部电梯同时运行的情况下,为了使多部电梯均匀的分布在所有楼层之间,我设计了一个初始方向和初始延迟时间,效果比多部电梯同时从一层开始扫描好很多。但作业3中我便放弃了这一做法,因为不同类型的电梯初始仅一个,没有必要刻意错开。

在本单元作业多线程同步部分的相关代码中,我没有使用wait方法。有同学可能会比较惊异于,不用wait()方法,难道不会造成暴力轮询、从而导致CTLE吗?事实上,我真的从来没有遇到过CTLE。之所以不使用wait方法,这其实是与我的电梯调度策略有紧密的关联性,同时我的请求队列的接口也很好的提供了相关的逻辑实现。具体参见1.3 电梯进程与请求队列接口概述

1.3 电梯进程与请求队列接口概述

对于LOOK算法,即使电梯内外都没有请求,电梯也不会停下来等待请求,而是根据当前方向上的请求情况继续决定向哪个方向运行。这种情况下,电梯到达某个楼层之后会进行一个返回值为boolean的hasRequest方法的判断。当请求队列中没有请求的话,只要返回false即可,此时电梯正常前行,实在不需要使用wait方法、等待下一个请求输入。事实上,即使当请求队列中有请求,如果该请求不是在电梯当前所在的楼层,在判断时也会返回false。

电梯内部也有一个内部队列。电梯开门后,电梯可以将主请求队列当前楼层存在的请求取出并添加到电梯的内部请求队列中,这代表了人员进入电梯;电梯还可以将当前内部请求队列中的请求移出,代表人员离开电梯。该内部请求队列和主请求队列其实是类似的,同样通过一个返回值为boolean的hasRequest方法来判断是否有人员要离开。

当上述两个请求队列中的只要有一个hasRequest方法返回值为true,电梯就需要在当前层开门。开门后,电梯将通过请求队列的getRequest方法取出需要上下电梯的请求,并进行执行(输出)。如果所有当前层可执行的请求都已取出,继续调用getRequest方法将会得到null。

电梯调度中的一大关键是电梯进程中的needReverse方法。该方法通过调用请求队列中的接口,实现了对电梯反向的逻辑判断。这也是区别与SCAN算法的重要之处。

关于请求队列,要写的有点多、且比较麻烦。在作业1中我采用了Arraylist来存储,而在作业2、3中我采用了HashMap,将请求与楼层进行绑定。比较诡异的一点是,在主请求队列中,我采用了FROM与Request绑定的方式;在内部请求队列中,我采用了TO与Request绑定的方式。这导致了两者都由同样的类来实现、共用同样的方法显得非常的奇怪。出现的情况就是,同样的方法对于主请求队列和内部请求队列来说含义可能是不一样的。所以我也说不好,这样的代码复用到底好不好。在作业1和作业2中我采用了同一个类来实现;而在作业3中,根据需要,我便将两个队列类彻底分开了。


二、SOLID原则 - 之于 - 作业3

2.1 SRP (Single Responsibility Priciple)

SRP原则意指每个类或方法都只有一个明确的职责。这一点上我认为我的作业3还算可以。主类只关注于输入、电梯类只关注与其运行与开关门、线程交互所需的容器我全部集中在Dispatcher类中进行集中管理、主请求队列和内部请求队列只关心队列自身请求的输入与输出。此外还有一个StopFloors类统一存放并管理不同类型电梯所能停靠楼层的全部静态数据、TargetFloorCal类统一存放计算请求目的楼层(换乘或直达皆可用)的全部静态方法。每个类职责清晰,没有出现一个类职责不明确的情况。

2.2 OCP (Open Close Principle)

OCP原则意指无需修改已有实现(close),而是通过扩展来增加新功能(open)。这一点我得承认我的第三次作业做的不好。我的第三次作业是在第二次作业的基础上直接进行的修改,有些地方改得也有点乱了;此外,由于我的电梯是自行调度,核心调度逻辑都集中在电梯类中,因此如果涉及到功能的扩展的话,必然需要对底层电梯内部的逻辑进行修改,而无法仅仅通过新增一些调度相关的类来实现功能扩展。

2.3 LSP (Liskov Substitution Principle)

LSP原则意指任何父类出现的地方都可以使用子类来代替,并不会导致使用相应类的程序出现错误。由于第二单元的三次作业我都没有使用继承,因此此原则没有涉及。

2.4 ISP (Interface Segregation Principle)

ISP原则意指一个接口只封装一组高度内聚的操作。由于第二单元的三次作业我都没有使用接口,因此此原则没有涉及。

2.5 DIP (Dependency Inversion Principle)

DIP原则意指高层模块不应该依赖于底层模块,两者都应该依赖于其抽象。由于第二单元的三次作业我都没有使用继承和接口,因此此原则没有涉及。


三、程序结构分析

  • 首先是UML协作图
2

因为我没有设计调度器进程,因此这个图对于三次作业来说是通用的。主线程将请求送到请求队列,电梯线程将请求取出并执行,简单又明确。

作业1

  • 下图为代码行数统计信息
statistic

作业1较为简单,所以代码行数不多。

  • 下图为UML类图

每个类各司其职,类之间关系清晰明确,电梯自调度很适用于一台电梯的情况。请求队列内部采用ArrayList的数据结构,对请求的管理不是很方便。

  • 下表为类的度量结果
class

Elevator中的公共属性是public final int UP = 1public final int DOWN = -1,以两个常数代表电梯运行的方向,且其互为相反数,方便电梯反向时状态值直接取反。

作业1仅一部电梯,较为简单,因此并没有使用继承。

  • 下表为方法的度量结果
method

Elevator类的arrive方法较为复杂,原因是我在arrive方法中进行了需要开门的判断与电梯门开关、人员入出的过程模拟。Elevator类的run方法也较为复杂,原因是我在run方法中进行了电梯宏观运行状态的判断和模拟。

RequestQueue类中的hasRequestFrom、hasRequestTo、getRequestFrom、getRequestTo四个方法复杂度较高,原因是我在其中传入了电梯运行方向,并依据电梯运行方向判断该方向上是否有请求,代码中有较多分支条件且各分支条件存在一定重复性,因此复杂度较高。

作业2

  • 下图为代码行数统计信息
statistic

比起作业1,题目需要更多电梯,需要对主类代码进行修改。此外,我还将请求队列由全部请求聚集在一起的ArrayList改为了依据楼层对请求分类的HashMap,并改写了相应方法,这也增加了部分代码。还有,我将输入线程与输出线程共享的数据放到了一个单例类Dispatcher中,将这些数据集中管理,增加了代码,使得整个程序结构更加清晰、明确。

  • 下图为UML类图

我认为我的作业2的整体结构也还是比较清晰的。比起作业1,本次作业新增了一个单例类来对输入线程与电梯线程之间的共享数据进行统一管理,这是一个进步。

  • 下表为类的度量结果
class

比起作业1,作业2的Elevator类新增了一个public final int MAXPSGER = 7,代表了电梯的最大容量。作业2五个电梯各参数均相同,因此仍然不需要使用继承。

  • 下表为方法的度量结果
method

复杂度高的方法仍然是作业1中复杂度就很高的方法,这与作业2中的逻辑比作业1中更为复杂有关;但同时也因为作业2的逻辑还不是那么复杂,所以就没有去主动降低这些方法的复杂度。

作业3

  • 下图为代码行数统计信息
statistic

代码量直线上升,因为作业3确实很复杂。

  • 下图为UML类图

由于是直接在作业2的基础上进行的迭代、而没有进行重构,所以感觉很多地方都是在打补丁。新添加的类全部由静态属性和静态方法构成、是为了完成作业3中的特殊需求而生的、是随时需要而随时创建的,而不是经过深思熟虑得到的结果。我想这样的代码在本次作业还可以使用,但如果在作业3的基础上还需要继续迭代就比较麻烦了。

  • 下表为类的度量结果
class

本次作业是可以采用继承的。但是我没有使用,原因是:本次作业是在第二次作业的基础上直接修改的,因此没有对架构进行大变动。不同类型的电梯的不同特性体现在构造函数中对这些属性的初始化,这使得构造函数有些臃肿。这也是为了不对架构进行大改的权宜之策。

  • 下表为方法的度量结果
method

作业3对方法的复杂度进行了优化:将逻辑复杂的方法拆分成独立的且有意义的独立方法、将复杂的布尔表达式封装到一个函数中。getTargetFloor的三个方法复杂度仍然较高,原因是其对换乘楼层进行了类似于枚举的返回值。


四、bug分析

本单元作业很幸运,三次作业均无未通过的公测样例和互测样例,因此本节内容为null。


五、发现他人bug策略

本单元作业1和作业3互测阶段我均未发现他人的bug。作业2互测阶段随意提交的一组数据hack到了一名同学的死锁bug,但是在本地无法复现。

事实就是我没有什么策略来进行测试,而我的三次作业的互测房间中同学们的bug真的很少,硬要说策略的话,就是瞎猫碰死耗子策略。

所以我认为,本单元的测试策略与第一单元测试策略的差异之处就在于,本单元的测试策略玄学了很多。据说有同学想要hack一位同学的bug,结束之后才发现想hack的同学没被hack,却奇妙地hack到了其他同学。而我hack到的bug在本地根本无法复现。第一单元作业的测试可以有确定的结果,可本单元作业的测试却真的有很多不确定因素,我想这大概就是差异吧。


六、心得体会

6.1 线程安全

在多线程程序设计中,线程安全无疑是很重要的。我在这三次作业中均未遇到线程安全问题,但这不意味着线程安全问题就不存在了,只是我碰巧避开了而已。

据我的观察,似乎同学们的线程安全问题可能主要发生在在wait和notify上。而我恰好在这三次作业中都没有用到wait和notify,这确实让我避免了很多潜在的问题。但是,wait和notify并不是线程安全的全部。我想,不是wait和notify造成了线程安全问题,一切线程安全问题都是由于对多线程的理解不够深入造成的

个人认为,在使用synchronized进行同步的时候,一定要想清楚一个问题:我锁住的是谁?对于synchronized修饰的方法,如果是非静态方法,锁住的是this;如果是静态方法,锁住的是class。对于synchronized语句块,锁住的就是括号内的对象。我个人非常不建议直接在方法上使用synchronized修饰,哪怕我需要在方法的第一行就写上synchronized (this)synchronized (class),因为我认为直接在方法上进行修饰的做法没有明确指出锁住的对象,非常容易引起混淆。而synchronized (...)这样的写法灵活性会更高,可以对更多的对象进行上锁。

此外真的非常推荐使用ReentrantLock,真的非常好用,也非常不容易产生混淆或迷惑。

6.2 设计原则

对于单一职责原则,我很努力地在去做。感觉大部分类确实也都是单一职责的,如果想要把这些类再细分我也真说不好能怎么分。也不能为了所谓的“单一职责”而将每个职责无限细分,这样也不是一件好事。

对于开闭原则,我的感觉是:如果在上一次作业的基础上扩展的话,一点都不去修改已有实现也确实挺困难的,感觉也不太现实。可能这个原则不是一个离散的评判标准,不能说做到就是1,做不到就是0,而应该采用“我做到了0.7的开闭原则”这样的说法。我认为这样还是比较合适的。

对于其他原则,本次作业不涉及,因此确实没有在作业中获得到关于这些原则的体会。希望之后可以渐渐对这些原则都有所体会吧。


七、写在最后

这是我第一次接触多线程程序设计,在此之前对多线程从未有过相关的了解。而就在这一个月不到的时间里,经过OO这一单元的学习、以及OS最近在讲的进程管理内容的学习,我对多线程程序设计这一方面的收获巨大。本单元三次作业的得分也都比较令我满意,也请容我内心小窃喜一下。希望OO接下来的两个单元作业我都能顺利完成,争取在学到知识的同时,也能保持好前几次作业已经取得的成绩。加油!

你可能感兴趣的:(面向对象第二单元总结 - 之 - 吾梯永不停 - 简体版本)