这三次做业所有是关于电梯的。主要锻炼了多线程编程的能力,以及了解一些调度算法的使用。html
[TOC]java
第五次做业,不是通常的简单,几乎人人满分的那种。dalao们甚至在30行内写出单线程调度就解决了。摸鱼划水的一次。本着练习多线程的目的,我仍是老老实实写了调度。只有一个请求队列,因为不考虑捎带,因此直接FIFO队列便可。因为当时对同步和锁的掌握很浅,为了求稳,对于同步访问(队列为空时等待)采用了轮询的办法。这样虽避免了死锁,但浪费CPU时间。正则表达式
架构:首先定义两个线程(输入、电梯),它们在主线程中启动,共享一个请求队列。这队列用线程安全容器LinkedBlockingQueue
。一方面是为了阻塞,另外一方面是为了保证队列长度(即size()
方法)的访问是一个原子操做。另外定义一个状态类WhenToStop
,用来指示输入是否结束。stopInputting()
方法用来通知两个线程输入已结束。算法
电梯的移动,是直接到目标楼层的,不须要考虑某一层掉头等的问题。编程
UML类图:安全
复杂度分析:多线程
Methods:架构
Method | ev(G) | iv(G) | v(G) |
---|---|---|---|
Elevator::Constructor | 1 | 1 | 1 |
Elevator.absSub(int,int) | 2 | 1 | 2 |
Elevator.closeDoor() | 1 | 3 | 3 |
Elevator.getOff() | 1 | 1 | 1 |
Elevator.getOn() | 1 | 1 | 1 |
Elevator.openDoor() | 1 | 3 | 3 |
Elevator.run() | 2 | 3 | 4 |
Elevator.toFloor(int) | 1 | 3 | 3 |
InputThread::Constructor | 1 | 1 | 1 |
InputThread.run() | 1 | 4 | 4 |
Main.main(String[]) | 1 | 1 | 1 |
WhenToStop.isInputting() | 1 | 1 | 1 |
WhenToStop.stopInputting() | 1 | 1 | 1 |
Total | 15.0 | 24.0 | 26.0 |
Average | 1.15 | 1.85 | 2.0 |
Classes:并发
Class | OCavg | WMC |
---|---|---|
Elevator | 1.625 | 13.0 |
InputThread | 1.5 | 3.0 |
Main | 1.0 | 1.0 |
WhenToStop | 1.0 | 2.0 |
Total | 19.0 | |
Average | 1.46 | 4.75 |
耦合度分析:ide
Class | Cyclic | Dcy | Dcy* | Dpt | Dpt* |
---|---|---|---|---|---|
Elevator | 0 | 1 | 1 | 1 | 1 |
InputThread | 0 | 1 | 1 | 1 | 1 |
Main | 0 | 3 | 3 | 0 | 0 |
WhenToStop | 0 | 0 | 0 | 3 | 3 |
Average | 0.0 | 1.25 | 1.25 | 1.25 | 1.25 |
复杂度控制仍是比较好的,没有一个方法的复杂度超过5。耦合度也不高。线程间通讯只靠两个共享对象。
时序图(线程间通讯机制):
此次须要可捎带,因此第五次做业那种只设置一个共享队列的方法不适用了。由于捎带请求必须同时考虑电梯外请求和电梯内乘客,并在合适时机掉头。
我查了一下可行的调度算法,包括:scan算法,look算法,ALS,还有FCFS,最近距离等。
scan算法是上下循环调度,相似于摆渡车、轮渡,缺点是没有乘客的楼层也要停。
look是scan的改进,没有乘客和请求的楼层不停,直接掉头。
ALS和FCFS太熟悉了,不用说。
还有一种是最近距离,也就是当电梯内有乘客时,乘客优先。无乘客时,距离当前楼层最近的请求优先。但这会出现某些请求“饿死”的状况。
综合以上几种,仍是look比较好。
基本思想就是,请求要分为电梯外请求和电梯内乘客两部分。电梯外请求按照出发楼层分类,电梯内请求按照目标楼层分类。每当电梯到达某一层时,检查是否须要上客或下客。下客的标准是到达目标楼层,上客的标准是他的请求方向和电梯当前运行方向(不是主请求的请求方向)相同。有则开门,无则甩过直接走。每当准备移动到下一层时,检查是否须要掉头(检查是否掉头是一个扫描过程,若是电梯内有乘客则不能掉头,不然扫描电梯外请求,若是当前方向上没有请求则掉头,若是两边都没有请求,则停下来等待)。
此次因为是单部电梯,因此直接复用上次的架构,不须要增长调度盘。线程安全上,考虑了用lock
和codition
来进行同步、互斥。这样灵活性比较高。
所用容器以下(直接粘贴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类图:
复杂度分析:
Methods
Method | ev(G) | iv(G) | v(G) |
---|---|---|---|
Elevator::Constructor | 1 | 2 | 2 |
Elevator.checkDirection() | 10 | 6 | 10 |
Elevator.closeDoor() | 1 | 4 | 4 |
Elevator.downOneFloor() | 1 | 3 | 3 |
Elevator.getFloor() | 2 | 1 | 2 |
Elevator.getOff() | 1 | 2 | 2 |
Elevator.getOn() | 3 | 5 | 5 |
Elevator.isEmpty(HashMap<Integer, LinkedBlockingQueue<PersonRequest>>) | 3 | 2 | 3 |
Elevator.moveOneFloor() | 1 | 2 | 2 |
Elevator.openDoor() | 1 | 3 | 3 |
Elevator.realFloor(int) | 2 | 1 | 2 |
Elevator.run() | 3 | 9 | 10 |
Elevator.upOneFloor() | 1 | 3 | 3 |
InputThread::Constructor | 1 | 1 | 1 |
InputThread.run() | 1 | 4 | 4 |
InputtingState.isInputting() | 1 | 1 | 1 |
InputtingState.stopInputting() | 1 | 1 | 1 |
Main.main(String[]) | 1 | 2 | 2 |
Total | 35.0 | 52.0 | 60.0 |
Average | 1.94 | 2.89 | 3.33 |
Classes
Class | OCavg | WMC |
---|---|---|
Elevator | 3.08 | 40 |
InputThread | 1.5 | 3 |
InputtingState | 1 | 2 |
Main | 2 | 2 |
Total | 47.0 | |
Average | 2.61 | 11.75 |
可见检查掉头的方法checkDirection()
的复杂度仍是比较高的,远远超过平均复杂度。这个方法要综合考虑乘客和外请求,代码量也比较大,达到了30行。另外,Elevator
类是实现核心功能的,可是包含了调度方法,因此复杂度也比较高。当时是为了方便,调度器和请求队列合一。
耦合度分析:
Class | Cyclic | Dcy | Dcy* | Dpt | Dpt* |
---|---|---|---|---|---|
Elevator | 0 | 1 | 1 | 2 | 2 |
InputThread | 0 | 2 | 2 | 1 | 1 |
InputtingState | 0 | 0 | 0 | 3 | 3 |
Main | 0 | 3 | 3 | 0 | 0 |
Average | 0.0 | 1.5 | 1.5 | 1.5 | 1.5 |
耦合度依然不高,由于两个线程通讯仍然只须要两个共享对象、一把锁、一个条件(监听器)。
时序图(线程间通讯机制):
此次又不同了,不只增长了两部电梯,并且增长了乘客数量限制、停靠层限制。因此,这决定咱们须要一个调度盘来把乘客请求分配到合适的电梯上去。而且,考虑换乘问题。
电梯类统一建模,可是增长一些属性,包括停靠层限制、核载、运行速度。利用(半)工厂模式,构造函数只传入电梯类型,根据电梯类型来决定这些属性的值。因为仍然采用look算法,因此能够复用上次的电梯,只须要对电梯运动状况和掉头的条件作一些调整。
架构就是三个电梯、一个调度盘,请求队列分三块,一块是原始请求,也就是刚输入的时候,没有通过调度器分配,能够理解为站在大厅门外。一块是分配后请求,能够理解为站在某个电梯门口。最后是电梯内乘客。
换乘不难解决,为了一劳永逸的防止出错,咱们把未完成的请求扔回原始请求中,也就是回滚。这样保证各个电梯互相独立。电梯只负责运送乘客,无论任何调度问题。这样保证耦合度低,不易出错。
另外的坑点,注意中止条件。这一次,使用共享对象unfinishNum
(自定义一个类SyncInteger,模拟原子整数),表示未完成的请求个数(从一个请求产生到它从电梯中出去并上到目标楼层为止,这段时间称做未完成)。当输入结束,而且未完成请求个数为0时,全部线程所有终止。再有就是注意输出互斥问题,由于输出函数对stdout是竞争关系,因此同一时刻只能有一个线程在输出,不然会致使输出穿插(这个穿插是指行内穿插)混乱。
三部电梯的调度,应该让它们尽可能并行,这一步由调度器Dispatcher
来完成。调度器也是一个线程,和输入线程、电梯线程并发。具体的调度优化,详见:https://www.cnblogs.com/wancong3/p/10739633.html
UML类图:
复杂度分析:
Methods
Method | ev(G) | iv(G) | v(G) |
---|---|---|---|
Dispatcher::Constructor | 1 | 1 | 1 |
Dispatcher.dispatch(PersonRequest,int) | 3 | 4 | 4 |
Dispatcher.fixedFloor(int,int,int) | 13 | 4 | 15 |
Dispatcher.run() | 3 | 11 | 12 |
Elevator::Constructor | 2 | 7 | 10 |
Elevator.checkDirection() | 10 | 9 | 13 |
Elevator.closeDoor() | 1 | 4 | 4 |
Elevator.downOneFloor() | 1 | 3 | 3 |
Elevator.fixedFloor(PersonRequest) | 12 | 4 | 14 |
Elevator.getFloor() | 2 | 1 | 2 |
Elevator.getOff() | 1 | 4 | 4 |
Elevator.getOn() | 5 | 6 | 10 |
Elevator.isEmpty(HashMap<Integer, LinkedBlockingQueue<PersonRequest>>) | 3 | 3 | 4 |
Elevator.openDoor() | 1 | 3 | 3 |
Elevator.realFloor(int) | 2 | 1 | 2 |
Elevator.run() | 3 | 11 | 12 |
Elevator.syncOutput(String) | 1 | 1 | 1 |
Elevator.toNextFloor() | 1 | 3 | 3 |
Elevator.upOneFloor() | 1 | 3 | 3 |
InputThread::Constructor | 1 | 1 | 1 |
InputThread.run() | 1 | 4 | 4 |
Main.main(String[]) | 1 | 7 | 7 |
SyncInteger.SyncInteger(int) | 1 | 1 | 1 |
SyncInteger.getValue(int) | 1 | 1 | 3 |
ThreadRunning.isRunning() | 1 | 1 | 1 |
ThreadRunning.stopRunning() | 1 | 1 | 1 |
Total | 73.0 | 99.0 | 138.0 |
Average | 2.81 | 3.81 | 5.31 |
Classes
Class | OCavg | WMC |
---|---|---|
Dispatcher | 5.75 | 23 |
Elevator | 4.33 | 65 |
InputThread | 1.5 | 3 |
Main | 7 | 7 |
SyncInteger | 2 | 4 |
ThreadRunning | 1 | 2 |
Total | 104.0 | |
Average | 4.0 | 17.33 |
复杂度较高的方法就是fixedFloor()
,用于寻找合适的换乘点。它采起了随机顺序的方法,而且循环搜索。
另外值得注意的就是,调度器类和电梯类的复杂度都比较高,多是由于使用了过多的共享对象。还有就是Main类的OCavg居然达到了7,也和初始化过多的共享对象有关。可是没有办法啊,原本电梯换乘是乘客考虑的,强行扔给调度器也是真的懒(哈哈哈~)。
耦合度分析
Class | Cyclic | Dcy | Dcy* | Dpt | Dpt* |
---|---|---|---|---|---|
Dispatcher | 0 | 3 | 3 | 1 | 1 |
Elevator | 0 | 2 | 2 | 3 | 3 |
InputThread | 0 | 3 | 3 | 1 | 1 |
Main | 0 | 5 | 5 | 0 | 0 |
SyncInteger | 0 | 0 | 0 | 4 | 4 |
ThreadRunning | 0 | 0 | 0 | 4 | 4 |
Average | 0.0 | 2.167 | 2.167 | 2.167 | 2.167 |
遵循SOLID原则,调度器和电梯功能分离,下降耦合度仍是有效果的,我以为不要管那些极限性能,这样设计才是最佳方案。系统稳定性比单个数据的性能重要得多。
SOLID原则:
一、单一责任:每一个类只管本身该管的事情。我以为这个是重中之重,电梯就是运输乘客,调度器才负责具体哪一个乘客上哪一个电梯。这样增长了可维护性,即便要改电梯也不会牵一发动全身。
二、开闭控制:哪些类支持扩展,哪些类是final的,哪些函数设置为对外接口,哪些函数是封装的。另外,一样的功能能够定义成抽象方法,支持不一样的实现。电梯其实能够这样作,把开关门、上下客和移动到下一层的方法做为抽象方法。只不过,一次做业中没有多种不一样实现的电梯,因此目前还不必这么作。
三、里氏替换:和自定义类的继承(is-a继承)有关。extends Thread
不是啥设计层面的继承,不用管。
四、依赖倒置:强调依赖关系中,高层模块的抽象。电梯固然不会依赖本身。(但这个原则在第三次做业,就是多项式、三角函数复合求导中相当重要,由于求导方法依赖于具体的表达式形式,只有把表达式类抽象出来才能获得语法树)
五、接口分离:说的是,不要用单一接口实现多功能。电梯的设计,远没有这么复杂。
时序图(线程间通讯机制):
若是按照测试的标准,这三次的强测没有错误。互测呢,根本没有针对性,很难发现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大法好!
此次测试和第一单元不一样的地方在于,不是简单工具能够解决的,必须靠本身分析题目逻辑来判断是否正确。
三次做业强迫我学习了多线程。其实最重要的收获就这一点。具体点就是线程安全考虑。只要是设计并发程序,时刻都要考虑线程安全。掌握了线程安全的几种方法:同步、条件锁、原子类、线程安全容器。
还有就是随机算法的认识,在数据良莠不齐或彻底随机的状况下,随机算法能显著提升平均性能。由于在实际中,最坏状况因为出现几率极小,每每不重要,这也是平摊分析的基本思想。
这三次做业,我感到OO通过改革比往届好得多了,除了前两次做业之外,已是真正的面向对象。类设计、多线程、并行编程在工程开发中仍是很是重要的。另外就是强测作得愈来愈好,互测已经不怎么是得分手段了,从而能真正体现出程序的质量(固然,有时候也会栽,强测炸的状况不是没有过,若是bug修复能捡回强测部分分就更好了)。
时序图工具:PlantUML Integration
安装方法:(这个是IDEA插件,喜欢用Eclipse的小伙伴自行去找,反正我是没找到)
一、IDEA-文件-设置
二、选择Plugins选项卡,单击上方Plugins Market
三、搜索PlantUML Integration
四、install
五、重启IDEA
使用方法详见:http://www.javashuo.com/article/p-uyqzdobb-ga.html
另外给你们推荐一个PlantUML详解:https://www.cnblogs.com/Jeson2016/p/6837186.html