OO第5-7次作业总结

OO第5-7次作业总结

这三次作业全部是关于电梯的。主要锻炼了多线程编程的能力,以及了解一些调度算法的使用。

[TOC]

1、设计分析

1.1、单电梯FCFS调度方案

第五次作业,不是一般的简单,几乎人人满分的那种。dalao们甚至在30行内写出单线程调度就解决了。摸鱼划水的一次。本着练习多线程的目的,我还是老老实实写了调度。只有一个请求队列,由于不考虑捎带,所以直接FIFO队列即可。由于当时对同步和锁的掌握很浅,为了求稳,对于同步访问(队列为空时等待)采用了轮询的办法。这样虽避免了死锁,但浪费CPU时间。

架构:首先定义两个线程(输入、电梯),它们在主线程中启动,共享一个请求队列。这队列用线程安全容器LinkedBlockingQueue。一方面是为了阻塞,另一方面是为了保证队列长度(即size()方法)的访问是一个原子操作。另外定义一个状态类WhenToStop,用来指示输入是否结束。stopInputting()方法用来通知两个线程输入已结束。

电梯的移动,是直接到目标楼层的,不需要考虑某一层掉头等的问题。

UML类图

image

复杂度分析

Methods:

Methodev(G)iv(G)v(G)
Elevator::Constructor111
Elevator.absSub(int,int)212
Elevator.closeDoor()133
Elevator.getOff()111
Elevator.getOn()111
Elevator.openDoor()133
Elevator.run()234
Elevator.toFloor(int)133
InputThread::Constructor111
InputThread.run()144
Main.main(String[])111
WhenToStop.isInputting()111
WhenToStop.stopInputting()111
Total15.024.026.0
Average1.151.852.0

Classes:

ClassOCavgWMC
Elevator1.62513.0
InputThread1.53.0
Main1.01.0
WhenToStop1.02.0
Total19.0
Average1.464.75

耦合度分析

ClassCyclicDcyDcy*DptDpt*
Elevator01111
InputThread01111
Main03300
WhenToStop00033
Average0.01.251.251.251.25

复杂度控制还是比较好的,没有一个方法的复杂度超过5。耦合度也不高。线程间通信只靠两个共享对象。

时序图(线程间通信机制)

image

1.2、单电梯可捎带

这次需要可捎带,所以第五次作业那种只设置一个共享队列的方法不适用了。因为捎带请求必须同时考虑电梯外请求和电梯内乘客,并在合适时机掉头。

我查了一下可行的调度算法,包括:scan算法,look算法,ALS,还有FCFS,最近距离等

scan算法是上下循环调度,类似于摆渡车、轮渡,缺点是没有乘客的楼层也要停。

look是scan的改进,没有乘客和请求的楼层不停,直接掉头。

ALS和FCFS太熟悉了,不用说。

还有一种是最近距离,也就是当电梯内有乘客时,乘客优先。无乘客时,距离当前楼层最近的请求优先。但这会出现某些请求“饿死”的情况。

综合以上几种,还是look比较好。

基本思想就是,请求要分为电梯外请求和电梯内乘客两部分。电梯外请求按照出发楼层分类,电梯内请求按照目标楼层分类。每当电梯到达某一层时,检查是否需要上客或下客。下客的标准是到达目标楼层,上客的标准是他的请求方向和电梯当前运行方向(不是主请求的请求方向)相同。有则开门,无则甩过直接走。每当准备移动到下一层时,检查是否需要掉头(检查是否掉头是一个扫描过程,如果电梯内有乘客则不能掉头,否则扫描电梯外请求,如果当前方向上没有请求则掉头,如果两边都没有请求,则停下来等待)。

这次由于是单部电梯,所以直接复用上次的架构,不需要增加调度盘。线程安全上,考虑了用lockcodition来进行同步、互斥。这样灵活性比较高。

所用容器如下(直接粘贴Main类源码,省略import):

public class Main {
    public static void main(String[] args) {
        TimableOutput.initStartTimestamp();

        final ReentrantLock reentrantLock = new ReentrantLock(); // 锁和条件
        final Condition condition = reentrantLock.newCondition();

        final HashMap<Integer, LinkedBlockingQueue<PersonRequest>> outerRequests
                = new HashMap<>(); // 电梯外请求
        for (int i = -2; i <= 16; i++) { // 这里的楼层做了特殊处理,以便电梯能连续运行,输出时再转换
            outerRequests.put(i, new LinkedBlockingQueue<>());
        } // 每层的请求都要初始化一个空队列
        final InputtingState state = new InputtingState(); // 指示输入是否完成

        InputThread inputThread = new InputThread(outerRequests,
                reentrantLock, condition, state); // 输入线程
        Elevator elevator = new Elevator(outerRequests,
                reentrantLock, condition, state); // 电梯线程

        elevator.start();
        inputThread.start();
    }
}

电梯内乘客的容器只在Elevator类中定义和使用,不需要共享。容器类型和电梯外请求相同。

UML类图

image

复杂度分析

Methods

Methodev(G)iv(G)v(G)
Elevator::Constructor122
Elevator.checkDirection()10610
Elevator.closeDoor()144
Elevator.downOneFloor()133
Elevator.getFloor()212
Elevator.getOff()122
Elevator.getOn()355
Elevator.isEmpty(HashMap<Integer, LinkedBlockingQueue<PersonRequest>>)323
Elevator.moveOneFloor()122
Elevator.openDoor()133
Elevator.realFloor(int)212
Elevator.run()3910
Elevator.upOneFloor()133
InputThread::Constructor111
InputThread.run()144
InputtingState.isInputting()111
InputtingState.stopInputting()111
Main.main(String[])122
Total35.052.060.0
Average1.942.893.33

Classes

ClassOCavgWMC
Elevator3.0840
InputThread1.53
InputtingState12
Main22
Total47.0
Average2.6111.75

可见检查掉头的方法checkDirection()的复杂度还是比较高的,远远超过平均复杂度。这个方法要综合考虑乘客和外请求,代码量也比较大,达到了30行。另外,Elevator类是实现核心功能的,但是包含了调度方法,所以复杂度也比较高。当时是为了方便,调度器和请求队列合一。

耦合度分析

ClassCyclicDcyDcy*DptDpt*
Elevator01122
InputThread02211
InputtingState00033
Main03300
Average0.01.51.51.51.5

耦合度依然不高,因为两个线程通信仍然只需要两个共享对象、一把锁、一个条件(监听器)。

时序图(线程间通信机制)

image

1.3、三部电梯协同调度

这次又不一样了,不仅增加了两部电梯,而且增加了乘客数量限制、停靠层限制。所以,这决定我们需要一个调度盘来把乘客请求分配到合适的电梯上去。并且,考虑换乘问题。

电梯类统一建模,但是增加一些属性,包括停靠层限制、核载、运行速度。利用(半)工厂模式,构造函数只传入电梯类型,根据电梯类型来决定这些属性的值。由于仍然采用look算法,所以可以复用上次的电梯,只需要对电梯运动情况和掉头的条件做一些调整。

架构就是三个电梯、一个调度盘,请求队列分三块,一块是原始请求,也就是刚输入的时候,没有经过调度器分配,可以理解为站在大厅门外。一块是分配后请求,可以理解为站在某个电梯门口。最后是电梯内乘客。

换乘不难解决,为了一劳永逸的防止出错,我们把未完成的请求扔回原始请求中,也就是回滚。这样保证各个电梯互相独立。电梯只负责运送乘客,不管任何调度问题。这样保证耦合度低,不易出错。

另外的坑点,注意停止条件。这一次,使用共享对象unfinishNum(自定义一个类SyncInteger,模拟原子整数),表示未完成的请求个数(从一个请求产生到它从电梯中出去并上到目标楼层为止,这段时间称作未完成)。当输入结束,并且未完成请求个数为0时,所有线程全部终止。再有就是注意输出互斥问题,因为输出函数对stdout是竞争关系,所以同一时刻只能有一个线程在输出,否则会导致输出穿插(这个穿插是指行内穿插)混乱。

三部电梯的调度,应该让它们尽量并行,这一步由调度器Dispatcher来完成。调度器也是一个线程,和输入线程、电梯线程并发。具体的调度优化,详见:https://www.cnblogs.com/wancong3/p/10739633.html

UML类图

image

复杂度分析

Methods

Methodev(G)iv(G)v(G)
Dispatcher::Constructor111
Dispatcher.dispatch(PersonRequest,int)344
Dispatcher.fixedFloor(int,int,int)13415
Dispatcher.run()31112
Elevator::Constructor2710
Elevator.checkDirection()10913
Elevator.closeDoor()144
Elevator.downOneFloor()133
Elevator.fixedFloor(PersonRequest)12414
Elevator.getFloor()212
Elevator.getOff()144
Elevator.getOn()5610
Elevator.isEmpty(HashMap<Integer, LinkedBlockingQueue<PersonRequest>>)334
Elevator.openDoor()133
Elevator.realFloor(int)212
Elevator.run()31112
Elevator.syncOutput(String)111
Elevator.toNextFloor()133
Elevator.upOneFloor()133
InputThread::Constructor111
InputThread.run()144
Main.main(String[])177
SyncInteger.SyncInteger(int)111
SyncInteger.getValue(int)113
ThreadRunning.isRunning()111
ThreadRunning.stopRunning()111
Total73.099.0138.0
Average2.813.815.31

Classes

ClassOCavgWMC
Dispatcher5.7523
Elevator4.3365
InputThread1.53
Main77
SyncInteger24
ThreadRunning12
Total104.0
Average4.017.33

复杂度较高的方法就是fixedFloor(),用于寻找合适的换乘点。它采取了随机顺序的方法,并且循环搜索。

另外值得注意的就是,调度器类和电梯类的复杂度都比较高,可能是因为使用了过多的共享对象。还有就是Main类的OCavg竟然达到了7,也和初始化过多的共享对象有关。但是没有办法啊,本来电梯换乘是乘客考虑的,强行扔给调度器也是真的懒(哈哈哈~)。

耦合度分析

ClassCyclicDcyDcy*DptDpt*
Dispatcher03311
Elevator02233
InputThread03311
Main05500
SyncInteger00044
ThreadRunning00044
Average0.02.1672.1672.1672.167

遵循SOLID原则,调度器和电梯功能分离,降低耦合度还是有效果的,我觉得不要管那些极限性能,这样设计才是最佳方案。系统稳定性比单个数据的性能重要得多。

SOLID原则

1、单一责任:每个类只管自己该管的事情。我觉得这个是重中之重,电梯就是运输乘客,调度器才负责具体哪个乘客上哪个电梯。这样增加了可维护性,即使要改电梯也不会牵一发动全身。

2、开闭控制:哪些类支持扩展,哪些类是final的,哪些函数设置为对外接口,哪些函数是封装的。另外,同样的功能可以定义成抽象方法,支持不同的实现。电梯其实可以这样做,把开关门、上下客和移动到下一层的方法作为抽象方法。只不过,一次作业中没有多种不同实现的电梯,所以目前还没必要这么做。

3、里氏替换:和自定义类的继承(is-a继承)有关。extends Thread不是啥设计层面的继承,不用管。

4、依赖倒置:强调依赖关系中,高层模块的抽象。电梯当然不会依赖自己。(但这个原则在第三次作业,就是多项式、三角函数复合求导中至关重要,因为求导方法依赖于具体的表达式形式,只有把表达式类抽象出来才能得到语法树)

5、接口分离:说的是,不要用单一接口实现多功能。电梯的设计,远没有这么复杂。

时序图(线程间通信机制)

image

2、bug杀虫和测试方法

如果按照测试的标准,这三次的强测没有错误。互测呢,根本没有针对性,很难发现bug。

来说说中测发现的bug。第五次就不说了,没有bug。

第六次,没提交的时候,就有严重的问题,停不下来。后来发现使用await()signalAll()用错了位置,在lock()unlock()之外使用,不仅无法结束,还报一大堆异常。

第七次更可笑。我知道三楼肯定会出一些小差错,就故意输入三楼的数据,结果上下往返停不下来。发现是换乘点有问题。再后来自己手动输入没问题,连文件输入都没测试,过于自信的提交,结果等了半分钟不出结果,已经有点慌了。第四分钟,不出所料,统统统统RTLE。

好吧,我的数据果然不行。借了它的两组数据发现问题很严重,一部电梯停下时另两部没停。干脆这样吧,当一个电梯将要结束时,通知其它电梯也停下来(因为此时电梯停止的条件已经满足,只要一接到signal立刻停止)。

有点怕了不敢乱提交,就自己用C写了一个数据生成器,又把官方的输入接口反编译了一下,自己加了定时输入。测了二三十组吧,没问题才敢提交(我知道复用上次的电梯,逻辑肯定不会有问题,就怕出在同步问题上)。

互测呢,感觉就是象征性地跑一跑交空刀刷活跃度。后来第六次身份公开,发现我和六系四大神仙一个组(具体是谁,我屋的人肯定知道),我一打擦边球进A组的菜鸡也能享有这样的待遇,难怪谁都发现不了问题。专门拜读了他们的代码,感觉一些强优化还是非常牛B的,我等算法小白甘拜下风。另外,每个人的调度思路都不同,不太可能通过读代码来针对性出数据,只能是测一测比较容易出错的地方,然后随机生成几个大量数据走压力测试。和第一单元不一样啊,第一单元完全是语法分析,这是多线程。没有了WF写起来真轻松,互测就八脸懵逼了。

在这里特别感谢老柴削面、东方削面HDL、DYJ、ZYY三位奆佬提供的定时输入方法、Special Judge和对拍器,虽然最终没用它们发现bug,但是至少解了燃眉之急。

多线程调试,尤其是当死锁的时候,最好的方法就是printf,你可以在printf的字符串中加一个debug,记得在提交的时候逐一删除(可以用idea的正则表达式替换,超级方便,所以我说你写个debug这样的标志)。线程安全相关问题,printf大法好!

这次测试和第一单元不同的地方在于,不是简单工具可以解决的,必须靠自己分析题目逻辑来判断是否正确。

3、收获

三次作业强迫我学习了多线程。其实最重要的收获就这一点。具体点就是线程安全考虑。只要是设计并发程序,时刻都要考虑线程安全。掌握了线程安全的几种方法:同步、条件锁、原子类、线程安全容器。

还有就是随机算法的认识,在数据参差不齐或完全随机的情况下,随机算法能显著提高平均性能。因为在实际中,最坏情况由于出现概率极小,往往不重要,这也是平摊分析的基本思想。

这三次作业,我感到OO经过改革比往届好得多了,除了前两次作业以外,已经是真正的面向对象。类设计、多线程、并行编程在工程开发中还是非常重要的。另外就是强测做得越来越好,互测已经不怎么是得分手段了,从而能真正体现出程序的质量(当然,有时候也会栽,强测炸的情况不是没有过,如果bug修复能捡回强测部分分就更好了)。

4、工具推荐

时序图工具:PlantUML Integration

安装方法:(这个是IDEA插件,喜欢用Eclipse的小伙伴自行去找,反正我是没找到)

1、IDEA-文件-设置

2、选择Plugins选项卡,单击上方Plugins Market

3、搜索PlantUML Integration

4、install

5、重启IDEA

使用方法详见:https://blog.csdn.net/River_Continent/article/details/79426064

另外给大家推荐一个PlantUML详解:https://www.cnblogs.com/Jeson2016/p/6837186.html

转载于:https://www.cnblogs.com/wancong3/p/10740540.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值