OO_第二单元总结

oo第二单元主要是Java多线程电梯问题,第一次作业是纵向电梯,第二次作业增加了横向电梯,并且可以动态增加电梯指令,第三次作业支持乘客换乘。性能分主要取决于不同的调度策略,即如何把所有乘客在最短的时间内送到目的地。

整体设计

一、所采用的策略

第一次作业策略,我将乘客依据不同的楼层,以及同一楼层的上行和下行请求设置了不同的队列,一共有20个队列,考虑到我们平常不会乘坐和目的方向运行相反的电梯,电梯中的乘客,要么同时上行,要么同时下行,在电梯没人时扫描本楼的所有队列,去接离电梯现在所在楼层最近的乘客,否则等待。强测大概有4个完全没有性能分(85)。

第二次作业,我参考了往届学长的博客,选择了LOOK策略,具体来讲是,电梯可以同时搭载不同方向的乘客。电梯到一个楼层,只要这个楼层有乘客,只要电梯人数没满,乘客就可以上电梯。电梯在运行时,如果本方向上还有乘客的目的地,或者本方向上的楼层还有请求,电梯就可以继续运行,否则,如果电梯乘客数为0,等待请求,如果乘客数不为0,就改变运行方向。

第二次作业同时采用了量子电梯。

量子电梯

假设电梯运行速度为0.4s,电梯在楼层a,那么电梯运行到下一层b所花时间为0.4s,其实可以等价于电梯在上一个状态(输出到达a层Arrive,或者输出在a层关门Close)后0.4s即可输出到达b层的信息。假设0.4s后b层有请求,那么我们不需让电梯等待0.4s,可以直接输出arrive b。

开关门期间可以上下人

电梯花0.2s开门,花0.2s关门,等价于在开门0.4s后输出关门信息。纸片人没有厚度,在0.4s时间内可以任意进入电梯,所以在电梯关门0.4s时间段,如果有乘客到达本层且电梯乘客未满,可以打断wait,让乘客进入。之后继续wait,不过wait的时间减少。

优先接目的地在电梯前进方向上的乘客

第二次作业做了这些优化后,发现性能提高仍不明显,所以又做了一个优化,把第一、二次作业结合,即乘客上电梯时,优先选择目的地是在电梯前进方向上的乘客。改了之后电梯性能大有提升。强测只有一个89,其他都是99、98的样子。

第三次作业采取了自由竞争策略,电梯在有乘客时都可以去接人,先到先得,增加了换乘策略。和同学讨论过如何保证两个电梯在同一层接人时,优先让更快的电梯先接到,没有什么结果。我考虑过设置线程优先级,但是优先级高的线程似乎只是分到的时间片多,和我的目的也不太一致。由于时间问题,我也没做相关的优化。

第三次作业,自由竞争策略很简单,结果也不错,课程组的性能标准给得又很温柔,强测没有低于99分的。

二、同步块与锁

同步块的设置和锁的选择

由于作业时间有限,我只采用了synchronized将对象作为锁,没有使用ReentrantLock和ReentrantReadWriteLock读写锁。对这两个类没有太多使用经验,担心出bug也是一个原因。

而且性能分主要还是由电梯移动时间和开关门时间决定,我也没再追求这方面的细节优化。

电梯在关门窗口期间使用了同步块,创建了一个新的共享变量lock,synchronized对象即为lock,lock拥有电梯的相关信息,lock也被等待队列共享,如果在开关门窗口期间有合适的乘客加入,等待类会通过lock唤醒电梯。

这样实现了关门的0.4s可以在有乘客到来时唤醒电梯,但可能还有更好的方法。

锁与同步块中处理语句之间的关系

  • 当两个并发线程,访问同一个对象的synchronized(x)同步代码块时,一段时间内只能有一个线程得到执行,另一个线程必须等待当前线程执行完这个代码块以后才能执行该代码块。
  • 当一个线程访问共享对象的一个synchronized(x)同步代码块时,另一个线程仍然可以访问该共享对象同一个方法中的非同步代码块。
  • 当一个线程访问共享对象的一个synchronized(x)同步代码块时,另一个线程对该对象中所有的其他synchronized(x)代码块,包括synchronized修饰的方法的访问将被阻塞。

三、调度器设计

大致思路是,输入是一个线程,当输入要求新建电梯时,直接用工厂模式新建电梯,如果输入的是乘客请求,就放入waitPeople队列。同时还维护了纵向电梯的请求类和横向电梯请求类,调度器从waitPeople里拿请求,如果判断是纵向请求,就放入纵向请求类,如果是横向请求,就放入横向请求类,如果是换乘乘客,就特判。

纵向电梯只从纵向请求类里面接人,横向电梯也只从横向请求类里接人。

四、架构模式

采用了流水线架构模式,主要针对换乘乘客,由于每座楼都有纵向电梯,但不是每层都有横向电梯,故会遍历所有现有的横向电梯信息,选出最佳中转楼层。由于纵向电梯的请求类ClassdedBuild和横向电梯请求类ClassedFloor是根据乘客的请求信息分类,故在电梯将请求放入两个类之前,要修改相关请求信息。
image

1.UML协作图

image

2.UML类图

image

Bug分析

一、自己的bug

强测均无bug

第一次作业被hack的点是我没有注意到官方输出包是线程不安全的,需要自己维护输出时间戳递增。

第二次作业没被hack,但是本地发现了一个逻辑错误。主要是else没特判,可能会到11层或0层。

第三次作业,在提交截止前两个小时,我才发现没有考虑到,横向请求也可能是换乘的请求(因为本层可以没有横向电梯)。我把横向请求最初都加到了横向请求类,如果接下来一直没有新加本层的横向电梯,这个人就出不来,程序也就停止不了,TLE

对于这类请求,最初应该加到ClassedBuild类,并且在特判是否是换乘乘客时增加这种情况。更改不到20行就可以修复bug。

但是最后两小时,提交间隔又是15min,比较急,我修改了好多代码,还是没赶得上。

本来以为强测铁定过不了,说不定全tle,互测都难进。哪曾料到强测所有的测试点都完美避开了我的bug。加上程序性能还可以,结果还挺好。

因为这个bug,互测被hack的测试点,80%都TLE。(没错我就是Saber)
难为大家了,都不敢放开手hack,o(╥﹏╥)o。

二、hack策略

随机生成数据黑盒hack,利用有限状态机检测正确性。

有限状态机主要是对输出答案正确性的检查,电梯基本上有移动,进人,出人几种状态。每一条输出指令都可以解析成电梯状态的改变,以此保证电梯的状态不会错误,不会出现没开门就进人等情况,同时也可以判断是否超载。

其次对比输入和输出,找出有哪些顾乘客没有被送到。

正确性检查的逻辑差不多就是这样。

只在第一次作业hack到了一个没有用迭代器,边遍历,边删减容器的同学。

第二三次在本地自动评测机跑了好久,要么没发现别人的bug,要么就是发现了bug,交上去,由于多线程的不确定性没有hack成功,我也没再多交。

还有一个和同学交流发现的bug,有的同学用的notifyAll次数太多,一些不需要用的地方也加上了,加上本身程序设计的特点,造成了轮询。

我删除了不必要的notyfyAll,发现CPU利用率确实下降了好多。

感想与总结

线程安全

对于共享对象基本上所有方法全加了synchronized,简单但是性能不如读写锁,以后可以尝试一下使用读写锁。

遇见过一次死锁问题,就是同步代码块包含了一个可以不包含在这个代码块里的调用,二者形成了死锁,把同步代码块变小一点就可以了。

层次化设计

一共有三个等待类,一个是Input线程输入的等待类,一个是调度器分派出横向电梯等待类,纵向电梯等待类。每个电梯配置了一个乘客类和策略类,电梯负责改变电梯状态,乘客类负责乘客上下电梯,策略类综合各种信息给出电梯下一状态。

感觉还可以再优化。

感想

自己的评测机还是不够好。第三次作业我随机生成了大量换乘乘客(指出发座和目的座、出发层和目的层都不相同)以及加减电梯的信息,没有发现bug。

吃完饭后回来想想,我是不是再调调乘客各种种类的比例?然后在截止前两小时发现bug。image

回想起我第一单元也是sum忘记判断负数,也是强测过了互测被hack惨了,历史总是惊人的相似。但我不觉得下一次能有这么幸运了。

得好好吸取教训。思考全面一点,自己设计的评测机也要多多关注各种边界情况。

其实第一次听课之前我对多线程还不是很了解,只是简单知道一些基本的线程创建运行知识。之后到图书馆恶补了Java多线程,就像马原老师说,实践是认识发展的动力,果然是有了ddl才最能激励人去学习。

希望以后可以规划好时间,可以做到预习叭。

posted @ 2022-04-27 08:46  eiang  阅读(39)  评论(2编辑  收藏  举报