传说中的多线程(魔鬼)电梯完成啦!
一、程序设计分析与基于度量的程序结构分析
三次电梯都统一地采用了生产者-消费者模型,每次在前一次的基础上进行添加,没有大规模的重构,可以说设计含有一定的可拓展性。
第一次电梯(单部多线程傻瓜调度(FAFS)电梯)
这是第一次接触多线程,十分迷惑。起初实在不知道什么是多线程,甚至写了单线程版本。后来通过查资料和多次尝试,根据生产者-消费者模型探索出了主线程(RequestInput)负责管理输入请求并存入Queue,另一个线程(ElevatorRun)模拟电梯的行为,从Queue中一次取出一条请求,根据规则将人送到目的楼层的模式。
我采用的是对请求队列的方法加锁的方法来保证线程安全。
还有一个比较主要的问题就是如何让电梯能够开始运行并且稳定地结束:在Queue中设置了flag属性,当没有取到指令时置为1,如果flag为1且请求队列为空则结束电梯,这样就可以保证电梯稳定结束运行。
因为是傻瓜调度,所以省略了调度器,直接在ElevatorRun的run() 方法中让电梯运行并且输出,这也就导致了该方法复杂度较高,应将输出信息和调度电梯都分离出不同的方法,提高模块化程度。
第二次电梯(单部多线程可捎带调度(ALS)电梯)
第二次电梯增加了捎带功能,使问题的复杂度上升了一个台阶。整体结构与第一次一致,但是没有真正理解设置调度器的用处,因此仍然在电梯类里面进行了取指令、分析捎带、电梯运行、输出结果这一系列工作,导致了出现了较为严重的bug。
在修复bug的过程中,我渐渐理解调度器是应该分析请求并给电梯传递消息,然后电梯根据这些消息去运行。
捎带策略:每次先去到主请求所在楼层,在送主请求的路上,到每一层判断是否当前楼层有人和主请求运行方向一致,如果有就让该请求进入电梯并且更新主请求目的楼层(目的楼层为最远楼层),当主请求到达目标楼层再进行新一轮循环。该方法只实现了最简单的捎带。在去接主请求的路上捎带,和不同方向的乘客的捎带都是可以优化的方向。
PS:其中同一楼层多人上下以及乘客先上还是先下是需要细致分析的,稍有不慎就可能有人被关在电梯里啦。
因为没有解耦所以Elevator和其中的run()方法还是负担过重,给程序带来了存在bug的风险。
第三次电梯(多部多线程智能(SS)调度电梯)
第三次是传说中最魔鬼的电梯:三部电梯停靠楼层不同,魔鬼3层要换乘,每部电梯还有不同的轿厢容量和运行时间。。。OMG,但后来经过将复杂问题简单化处理,可以转化为之前电梯的升级版(设计最重要!避免重构!)
采用的策略:在调度器里面设置了三个请求队列,每次ReqInput取到一个指令就放入Dispatcher里面按照一定的换乘规则分成能够直达的请求,然后放入相应的电梯请求队列,在main() 方法中创建三个电梯,它们有不同的属性,每个电梯根据相应的调度器运行,捎带策略与第二次电梯一致。
优点是容易实现,简洁清晰,不需要重构;缺点是分配请求机械化,性能不优。
本次作业已经尝试大量降低耦合,但复杂度高的是run()方法和判断捎带的方法,这也是不可避免的,争取以后通过实践能继续提高结构化程度。
三、程序bug分析
在三次电梯作业中,第一次简单电梯和第三次多部智能电梯没有在强测和互测中被发现bug,第二次捎带电梯强测中被发现五个同质bug。
出现bug的原因:设置了请求队列类Queue,其中包括getone()方法,从队列中取出特定元素,并将其从队列中删除,如下
synchronized PersonRequest getone(int i) { PersonRequest temp = queue.get(i); queue.remove(i); length--; return temp; }
而在判断当前楼层是否需要捎带时,又使用Queue中的getqueue()方法,将请求队列直接取出,如下
synchronized ArrayList<PersonRequest> getqueue() { return queue; }
当发现该请求需要捎带时,在Elevator中使用getone()方法取出,但是此时Elevator中取出的队列没有更新。
bug修复:将遍历过程放入Queue中,建立passreq(int floor, int ud)方法,可以保证请求队列的同步更新,如下
synchronized PersonRequest passreq(int floor, int ud) { PersonRequest per = null; if (queue.size() != 0) { for (int i = 0; i < queue.size(); i++) { if (queue.get(i).getFromFloor() == floor && ud == judgedrc(queue.get(i))) { per = queue.get(i); queue.remove(i); length--; break; } } } return per; }
出现问题的根本原因是对象之间没有降低耦合。调度器对象中存储请求队列,一系列和调度有关的功能都应该由调度器完成,否则就可能造成线程控制和线程安全方面的问题。而多线程一旦出现这种控制类的bug不容易自己检查出来,后果十分严重。
四、发现别人bug采用的策略
第一次大家的程序都没有什么问题,之后的两次借用了同学分享的可以定时投放的测试工具,构造了请求数较多,情况较复杂的数据,发现了一些电梯调度问题和不能稳定结束的问题。非常感谢同学的分享,希望自己以后也可以做类似的测试工具。
五、总结
这一单元主要学习了多线程问题,较为复杂且不容易理解,但多线程在现实生活中是十分有应用价值的,应该继续通过应用加深理解。如何保证线程安全是十分重要的问题,可以采用几种加锁方式,每种方式都有其特点,还要避免死锁。
这一单元作业都采取了相对简洁清晰的思路完成,代码实现较为容易,但是相对性能不高,还应该在保证正确性的基础上探索优化性能,不能得过且过。
设计注意可扩展性可扩展性可扩展性!
多思考 继续加油!
原文地址:https://www.cnblogs.com/peggyhss/p/10753457.html