OO_Unit2

第二单元作业总结博客

第二单元的多线程作业也告一段落了。犹记得第一单元我的总结词汇是“灾难”,那么第二单元我的总结词汇就是“煎熬”。有多煎熬呢,大概就是林俊杰开口唱煎熬那么煎熬吧。光是第五次作业我就花了整整两周(整整两周的含义就是我扔掉了包括但不限于OS,离散数学,冯如杯等所有的工作,每天除了上课的时间就是打开电脑对着IDEA那个灰不拉几的屏幕抓狂),重构了四次,提交了18次,为了问题多加了3个好友,才ac了全部的强测点。虽然后两次作业过的相对容易一些(至少比第一次容易很多),但也直接导致了这一个月来我看到synchronized,甚至看到电梯就各种生理不适。
但总结起这个单元,还是要比第一单元好了几个数量级,毕竟这每一行代码都是我一点一点敲出来的,每个bug也是一点点修出来的,也没有像之前那样,用投机取巧的预解析,也没有一个主类到底,一个月来,在各种想要吃掉电脑屏幕的崩溃后收获也是蛮大的,对多线程还是有了一丢丢的理解,总而言之,对自己的进步还是很欣慰

三次作业分析

因为要求在这:

  • 总结分析三次作业中同步块的设置和锁的选择,并分析锁与同步块中处理语句之间的关系
  • 总结分析三次作业中的调度器设计,并分析调度器如何与程序中的线程进行交互

我就在这里一并分析了

  • 第一次作业

    第一次作业纯纯的“摧残心灵”,说的倒好,“可以采用任意的调度策略”,只要把人送到目标位置即可,但还是得遵循个策略吧,总不能写个傻瓜电梯,一次就装一个人吧?
    您别说,我开始写的还真是个傻瓜电梯,还依靠他完成了第一次作业(毕竟清明节要出去玩,不能一直圈学习写代码),而且周二就写出来了,但思路完全偏了,所以重构了好多次,还经历了无数的破防瞬间。(想想都是泪,这血的教训也告诫我们,思路不清晰千万不要急着去写,更不要为了出去玩草草的动笔,这样只会改起来更麻烦)。
    讲我的错误思路实在是没什么大意义,而且万一您看了我的思路后产生了“有道理啊”的共鸣,那您也危险了。为了不引起恐慌,我只谈我的正确思路。该题的思路也比较直观,就是输入一条指令,由调度器扔给电梯,而每个电梯分配一个等待队列,由电梯自行决定怎么处理这些多事的家伙(bushi。思路大概如此,细节的到后面架构部分再讨论。
    再谈一下遇到的一些困难。首先便是线程同步的问题。严格来讲倒也不存在线程冲突,我加我的新指令,你跑你的电梯。但我开始是用Arraylist存的等待队列,我没有想到的是这Arraylist实在是太娇气,娇气到对他进行遍历的时候完全不能动他,要不人家啪的一下就给你抛异常,很快啊。如果说撵人将完成的指令删除还可以先将指令存起来,遍历之后再删除,但调度器可是完全不给面子,它可不管你是不是在遍历,进来一条新指令就直接丢进来,然后很快就是
    Exception in thread "main" java.util.ConcurrentModificationException
    at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:907)
    at java.util.ArrayList$Itr.next(ArrayList.java:857)
    at com.suyl.demo.test.ListTest.main(ListTest.java:21)
    
    于是我学习各种加锁,线程同步,可惜不是实现不了捎带,要不就是电梯一会吃人一会生殖。好容易都解决了,心满意足的扔进去一个数据,那天杀的Exception又来了,总之那段时间我的程序就像这样:

    最后,在一番深思熟虑后,我做出了一个伟大的决定:换容器!
    明明JAVA有线程安全的容器CopyOnWriteArrayList,而且用法也和Arraylist一毛一样,只是这个容器随便你蹂躏,只要不是爆范围这种,都不会抛异常。于是这个让我头疼了几个日日夜夜的问题,就以这种方式解决了。
    这里还是对线程安全做一个解释。因为我第一次没有对调度器单开一个线程(其实在后面也没有单开,)可以认为调度器和主线程是一体的,因此一个队列受两个线程在控制,主线程和相应的电梯线程。线程安全即两个线程都要操控这一个共享对象时,它的行为不会出现很诡异的错误。(你甚至不需要sychronized)
    开开心心交一次,CTLE。人顿时麻了,检查一下,没有用到wait-Notify机制,导致没有指令的时候CPU还在反反复复问,走不走,走不走,真不走啊??简而言之,就是轮询。那就wait吧,但这也就引出了第二个足以威胁我电脑屏幕的问题(别问为什么是电脑屏幕,程序行为不对的时候我第一反应是砸屏幕而不是找bug),如何结束整个程序?主线程容易,一个null进来直接挂掉就可以了,但子子孙孙无穷尽也,其他线程不结束,程序也结束不了。但这个该死的null进来的时候,这些线程们不一定在干什么:可能在跑电梯,也可能在等待,于是我设置了一个结束信号,这个结束信号相当于一个全局的变量,将它置真的时候会同时NotifyAll,这样等待的线程就被唤醒后马上挂掉(类似落地成盒),正在跑的指令会在跑完这一条后再次进入循环,然后挂掉。(如下)
    while (true) {
            synchronized (this) {
                if (isEnd && requests.isEmpty() && hasIn.isEmpty() &&
                        waitingQueue.isEmpty()) {
                    return;
                }
                if (requests.isEmpty() && hasIn.isEmpty()) {
                    try {
                        wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
            operate();
        }
    
    最后便是调度算法。我只能讲,要不是听信了指导书的谗言,我可能一周就做出来了。指导书推荐的ALS真的是费力不讨好的典型代表,特别不好实现,出了bug特别不好复现,而且性能还不占优。后来我听了某人的某学长介绍的LOOK算法,觉得自己捡了个大便宜,然而研讨课发现我们组6个人5个人都在用look算法......(人类进化不带我系列)
    LOOK算法便很简单了,它是SCAN算法的改进,scan是电梯只能从底层到顶层或者顶层到底层,look就是如果电梯运行方向没有指令,就转向。但针对这个“指令”,还可以细分,比如电梯在5楼,有一个家伙要从4楼到6楼,那么运行方向上有个6楼,但这有什么意义呢?所以我在每个电梯分配了2个队列,一个是已经在电梯内的,一个是还在电梯外傻等的。电梯完成的任务也就分成了两个:去送电梯内的人,去接电梯外的人。当然等待条件也就变成了:两个队列同时为空。不然的话如果电梯外没人但电梯里还有人,那么里面的倒霉鬼就要报警了。。。
    第一次大概如此,更细的见下文。
  • 第二次作业

    第二次作业应该说难度很低,也可能是第一次折腾了两周的架构漂亮,首先就是加了一个很高科技很超时代很赛鹏搏客的东西——横向电梯,它可以在两座之间以超音速的幻影速度飞行,但层数只能限制唯一,要不就真成了过山车了。另外还可以增加纵向电梯的个数。针对这两个问题,我分别做了以下处理:
    对于横向电梯,干脆当成另外一座楼的电梯处理,因为这里有个条件,每个请求只能在同一座楼或者同一层移动(当然,第三次的换乘已经呼之欲出了,但本着“苟一周是一周的原则,我选择装傻),那就干脆把人,请求,电梯和第一次的电梯完全分离,相当于15部电梯在跑。在类命名上,我也别出心裁的选择了Travel(为什么这么叫呢,我想在楼上旋转这个东西我也就在上海的东方明珠见过旋转餐厅,不像那些坐纵向电梯的苦逼上班族,能在同一层转圈圈的人八成是来玩的,那就让他们尽情Travel吧)
    然后便是纵向电梯,针对此我采用“车轮策略”,一人负责一个,如果轮完了就从头来,实现起来也很简单,因为一切都是调度器实现的,所以在指令被丢给电梯之前,调度器已经分配好了,这里就不加赘述了。
    最后还有一个问题需要解决,就是横向电梯怎么跑。我在此一定要介绍一下被我废弃了的方法。类似look算法,其实电梯在运行的过程中无非就是判断哪种策略更优,怎么做到走最少的路,完成更多的请求。横向电梯运行也就是两种方法:ABCDE(以下顺时针),EDCBA(逆时针),那么电梯准备完成下一步运行时,就要trade-off一下是顺时针跑合理还是逆时针合理,于是我设置了两个cost,如果电梯顺时针方向完成请求(接到电梯外的和送到电梯内的)需要多少步,逆时针同理,对他俩进行比较,哪个小,就怎么转。
    写完后我感觉这个方法简直太完美了,中测强测也顺利全过,然后就是在A房被干成筛子。其实也很容易想到为啥,假设现在顺时针的cost较小,电梯顺时针转一下,转过去以后还需要判断,恰恰这时逆时针小了,电梯又回到了原来的位置。而恰恰整个过程没人进也没人出也没新指令,两个cost还是原来的cost,顺时针又小了,电梯逆时针转,逆时针小,电梯逆时针转,顺时针小,电梯顺时针转......一个电梯就被设计成了一个活塞发动机。于是我忍痛割爱,让电梯只朝着一个方向旋转,结果令人暖心:竟然性能与我的伪look相差无几!!!想来定是电梯横向运行时间较短,因此影响不大。(但我还是放不下我的算法,有想到如何解决的dl可dd我)
  • 第三次作业

    第三次作业不管是对课程组居心叵测的合理推断,还是觉得写了好几周电梯自己跑自己的不甘心,总之换乘是来了。此外还加了一些我看起来很无聊的限制:速度,容量。还对横向电梯的停靠楼座做了限制,用二进制数的方式告知那些楼座可以停靠(不知道道灵感是否来源于北航新主楼2层不能坐电梯的反人类设计)。而且最开始默认一楼有一个横向电梯,因此如果你足够阴暗的话, 可以让他们全去一楼换乘。当然你的性能分也会因为你的不善良被扣掉。
    第三次作业可以说是经历了第三次屏幕危机。和第二次比,大概有三个问题需要解决,先介绍对屏幕相对友好的问题:
    在哪层换乘?这里指导书也提供了一种比较可靠的做法:该楼层的横向电梯存在(存在指目的楼座和出发楼座都能停靠)且换乘代价最小(两个高度差加起来最小),就选择它。我做优化题一向推崇暴力解法,况且该题限制电梯最多20台,就是全扫一遍,按我CPU的尿性,也不过几毫秒而已,那就干脆逐层逐台遍历,找到那个最优解,这样换乘楼层以及对应的电梯也就得到了,一个指令也就分裂成三个指令:竖着到换乘楼层,做Travel到目的楼座,再从换乘楼层竖着到目的楼层。
    说来容易,指令可以“分裂”,但人不能分裂。当一个人完成其中一个步骤(比如下了横向电梯),如何优雅的通知下一步的纵向电梯去接他?我的方案是都在调度器完成,但调度器需要上一步的电梯发出一个信号,它才能把下一步的方案抛给另一部电梯。但这里还有一个问题:因为我的调度器本身不是线程,它保留不了指令的信息,也就是说,当第一条指令进来后,它将指令分裂,将第一步指令丢给第一部电梯,当第一步完成后,给调度器一个信号通知发出第二步指令,但此时调度器早就不知道处理了多少条指令了,这条指令的信息也就尸骨无存了。
    那么既然调度器不靠谱,“打铁还需自身硬”,就让指令自己保存。我再指令类中增加了“换乘楼层”这一属性,然后由调度器去在三个楼层信息中挑两个发给电梯,再由电梯返回给调度器信息,调度器再挑两个...听起来就很恶心,(也幸好这是第三次作业,架构神马的也就摆烂了)实现起来也超级容易出错,我大概尝试了10+次,才在IDEA跑对。
    我以为这就完事了,于是我把数据扔到投喂包里,自信满满等着正确答案出来,结果我的屏幕又差点遭殃了:以一条的指令为例,它完成了第一步,整个程序突然就结束了。什么情况?想一想便容易想通:当调度器扔第一步指令时,其他所有线程都处于歇逼状态,这时候null进来了,其他线程一看自己没有请求,还到下班点了,我管你未来会不会要求加班,到点没活了我就润!于是第一步完成了以后,老板(调度器)想扔给第二部电梯一个指令,发现已经跑路了,当然就完成不了后面的了。
    于是我又遇到了第三个仿佛曾经出现过的问题:如何结束线程?曾经的问题是结束不了,现在是求他别那么快结束。在一番思想斗争下,我给每个电梯开了第三个队列:量子队列,也就是我每条指令一进调度器,调度器就给分裂好了(所以我指令的换乘楼层属性也就可以删掉了),除了第一条扔进等待队列,其余两条都扔进量子队列,量子队列的含义便是,现在电梯没有这个指令,也要假装不知道有这个指令,但未来的某一天这个指令一定会成为真正的指令。那么调度器在一步完成后要做的事就变成了:把personId相同的下一步指令从量子队列放入等待队列,为了能让它优雅的结束,结束判断条件也要改:(但等待判断条件不能改,因为即使量子队列不为空,电梯也不能执行这些指令,否则就会轮询):
    while (true) {
            synchronized (this) {
                if (isEnd && requests.isEmpty() && hasIn.isEmpty() &&
                        waitingQueue.isEmpty()) {
                    return;
                }
                if (requests.isEmpty() && hasIn.isEmpty()) {
                    try {
                        wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
            operate();
        }
    
    终于,我的三次作业全部圆满结束(烟花)。

程序结构

就说我代码风格拿了100分我骄傲了嘛

  • 类图


    为了解决问题方便(省事)我只有两种跑起来的电梯是线程,再加上主线程,一共三种,其他的都是来了就处理。先说这两个线程,无非就是电梯的基本操作(开门,关门,上人,下人,运行),但这每一步都是调用的其他类(ElevatorRunning,记录电梯此时此刻的状态,包括乘客数,当前楼层等等;以及Elevator类,负责输出各种行为)。因此Control类与其说是一个电梯线程,不如说是每个电梯自身的调度器,它负责处理整个电梯的请求队列,并发号施令给电梯,电梯只需要一次完成它发出的一个命令即可。(Travel和Elevator几乎一样,在此不再赘述)。
    (然后我最终没有听取rwg老师的意见,弄一个专门处理输入的线程),我处理输入都是在调度器中实现的。另外可能还需要改进的部分是,应该让纵向电梯和横向电梯同时实现一个接口,而不应该把彼此分的那么独立,这样不利于线程之间的通信。另外还有,第三次作业用流水线架构应该会更合理一点,还有,rwg老师倾情推荐的单例模式给调度器用可能更漂亮一些(看来研究的越多就会觉得不足越多,第一次我认为我的架构完美无缺
  • 协作图(不知道是个啥,直接放图吧)



bug分析

  • 第一次作业

    第一次作业的第一次强测就不具体分析了,因为写的是傻瓜电梯,想不RTLE有点困难,想WA更困难..
    就来谈谈重构完成以后的问题吧.首先还是策略问题,在自己尝试多次仍然RTLE后,陈一文dl给我的意见是"把人尽量塞满",我开始还一脸无辜:我塞满了啊.后来一想,我是先上人后下人,上人的时候直接判断有么有超过容量,但没有考虑到下人后还会腾出一些空间,等于还是没有塞满.这个问题相当好解决,先下人再上人就可以了(这也符合上电梯的礼让下电梯的的中华传统美德).
    第二点是我的电梯到达一层后,如果同时有上和下的需求,我的电梯会像傻子一样开一次再关一次,由来是我下人和上人之后都分别判断了一次状态,因此我直接设置了俩状态,打开状态(可以上下人),以及关闭状态(不能上下),这样只要电梯决定打开了,就给所有人400毫秒的时间,要上赶紧上,要撤赶紧撤,也节省了400毫秒.
    终于,在我对周围同学,助教的摧残和自我摧残下,第五次作业历经两周时间,终于ac了所有点.
  • 第二次作业

    第二次作业相对顺利,周四晚开始做,周五只有晚上军理课做了两节课,回去又做了一会就全过了,强测也全过,结果就是在A房被暴打(hack了4次).期间还有一个小插曲,就是我想扔数据刀人,但刀人还需要输出,我就在自己的程序上跑了一下,然后就把自己刀了......
    bug在前面也说了,包括我刀自己的数据,会出现活塞运动的情况.直接让他朝一个方向转,就可以了,性能差了半秒不到.
  • 第三次作业

    第三次虽然写的很艰难,但过的很容易.应该第二次交就全过了,第一次我是乱交的(我可不是乱交的啊,***,训练有素),但强测还是没全过,有两个RTLE,是抛得数组越界异常,还有一个CTLE是我没想到的.
    先解决数组越界那个,我把70个指令一起扔进去,一个一个删,终于定位到了异常的那条指令.再一点点跑,发现有一个地方貌似写错了:
    travel = travels.get(transferFloor).get(travelTurn[i]);
    应该是
    travel = travels.get(i).get(travelTurn[i]);
    因为前面有一句话
    i = transferFloor - 1;
    想来应该是把楼层和数组下标搞混了,因为楼层是从1开始的,但数组下标从0开始.想着交一次试试,居然全过了,那个CTLE可能也是这里跑死循环了吧.(神奇的是,这么大的bug居然没人hack我,可能大家都懒得hack了吧)

第二单元也算是神奇的经历了,ABC房都待了一遍,发现A房的同学是真的狠,C房的同学也是真的摆

hack策略

不同于第一单元,第一单元hack就是构造各种恶心的数据,这单元就是堆好多好多的指令,一股脑扔进去.
我还有幸在A房🔪了一个人,但应该是异常所以RE了.

心得体会

这个单元过的属实煎熬,包括自己第五次作业没做出来时,有段时间甚至想过摆烂,但想起来时还是寝食难安,还因此耽误了OS的学习.但我也意识到了一个良好架构的重要性,第五次费尽心思维护一个漂亮的架构,后两次扩展起来轻松很多

回想起这个单元的学习,最令人头疼的也就是线程安全问题了.虽然最后拿一个线程安全的容器解决了,但很多地方还是不太会用锁,一加大概率会错,但欣慰的是我现在不是锁出错就乱加锁去尝试了,而是去分析为什么会出错,去拿调试器仅有的功能去调试,就像rwg老师说的那样"你的多线程也就学了个皮毛,但现在只要掌握怎么wait,怎么notify就够了."
另外,这个单元也让我更加深刻认识到"分而治之,各司其职"的重要性,(现在看我第一次的一个类,简直是反面的反面),每个类只让做自己的事情,降低耦合性,(最后一次架构其实没那么好,也可能是因为是最后一次所以摆了).当然这个单元也告诉我,设计的时间永远要大于实现的时间,不想好就去写,代价会很惨痛.
最后,我还要隆重感谢一下中华第一好助教--廖纪童学长.前前后后被我折磨了将近一个月,每天教我怎么debug,怎么复现bug,解决异常,还亲自帮我看代码(泪目).
这是我俩第一次讨论电梯的题的时间:

这是我俩最后一次讨论电梯:

我还要感谢魏祎同学,每次在我抓狂崩溃时都想办法保住我的屏幕安慰我,还有许许多多在这一个月里被我突然加好友骚扰问题的同学.(听我说谢谢你,因为有你...)
终于,这个主题为电梯的月:

posted @ 2022-04-30 17:13  YiWforever  阅读(57)  评论(4编辑  收藏  举报