北航OO第二单元总结

北航OO第二单元总结

前言

本单元的作业为多线程的电梯调度问题,主要考察java中多线程的运用以及线程同步与锁的设置。相比于第一单元作业时的手忙脚乱,本单元作业我明显感觉要游刃有余一些,一方面得益于之前的锻炼,提高了编程能力,另一方面也是因为本单元作业设置的并不复杂,在不考虑复杂调度的情况下实现起来还是较为简单的。

整体设计思路

在设计上,本单元作业我采用的是生产者-消费者模式,主要分为三类线程:

  1. 生产者线程(InputThread):也即输入线程,每当有乘客到来时,视为生产了一个请求,随后InputThread将其加入等待队列中,待电梯处理(消费)。
  2. 消费者线程 (Elevator):也就是电梯线程,负责“消费”等待队列中的乘客,将其送往正确的楼层。
  3. 调度器线程 (Scheduler):负责等待队列中乘客的分配问题,解决乘客要做哪一步电梯的问题,虽然在第一次作业中作用并不明显,但是随着电梯线程数量的增加显得愈发重要。

设计中产生了两类共享对象:

  1. 等待队列(waitingQueue):最重要的共享对象,Input线程需要往里面加乘客,调度器需要从中取出乘客。
  2. 电梯的待乘队列(passenger):每一部电梯所需要接送的乘客队列,调度器需要向其中增加乘客,而电梯完成乘客请求后需要从中删除乘客。

本单元作业我是在思考好了以上几点之后才开始写的,三次作业都采用了这一架构,非常好用,明晰了整体的分配模式与共享对象以后,就只剩下细节的打磨了,说实话有了一个整体方向以后,代码写起来真的挺快的,本单元作业也是又一次让我深刻体会到了设计阶段的重要性。

第一次作业

作业要求

实现对电梯接送乘客的模拟,分别要实现Morning,Night,Random三种模式,且本次作业只设置一部电梯。

思路分析

吃过第一单元的亏之后,这次我总算知道要长远考虑了,既然这次只有一部电梯,那下次肯定就有多部电梯了,所以我写的时候就默认有n部电梯,最后交之前将参数改为1即可,事实证明这样做是非常正确的,让我后面的作业省了不少时间。

定义了以下几个类:

  1. MainClass:主类,负责几个线程的开启工作。
  2. InputThread:线程类,负责数据输入。
  3. Scheduler:调度器,线程类,负责进行乘客的调度。
  4. Elevator:电梯,线程类,模拟电梯的运行,并实时输出情况。
  5. Passenger:乘客类,储存了乘客的ID,起始楼层,目标楼层等信息。
  6. PassengerQueue: 封装了一个乘客类的ArrayList。
  7. WaitingQueue: 等待队列,储存了目前在电梯外等待的乘客。
  8. TestInput:调试时用于模拟定时输入的输入类,也是线程类。

各个类的定义还是很清晰的,实现起来也并不复杂,难点主要在于两点:共享对象的加锁问题和线程的结束条件问题。这两个问题困扰了我很久,主要还是因为第一次接触多线程的编程,很多语法与多线程设计模式都不清楚,最终还是在周五下午的实验代码的指导下才知道如何操作。

关于线程的结束条件

其实只要抓住每个线程的本质加以分析,判断好每个线程结束的指标就可以了。

  1. InputThread线程在输入结束后跳出循环即可结束。
  2. Scheduler线程的结束主要有两个指标,一是判断当前InputThread线程是否结束,我给WaitingQueue设置了一个isEnd属性,当Input线程结束时,将isEnd赋1,以此通知其他线程InputThread已经结束; 二是判断当前waitingQueue是否已经清空。当两项指标都符合时,说明当前所有乘客都已经分配给了电梯,且以后也不会再有新的乘客输入,可以结束本线程了。
  3. Elevator线程的结束有三个指标,前两个指标与第二点的相同,第三个指标就是电梯乘客是否已经送完。三个指标完成后,意味着一切都结束了,电梯使命结束。
关于共享对象的加锁

synchronized 的使用是本单元作业的最核心问题,哪些对象是共享对象?如何正确的加锁?如何避免死锁?每一个问题都要慎重考虑,在加锁的问题上任何一个细微的错误都会导致多线程中发生一些难以分析的现象,死锁都还算好的,我室友有一次电梯直接遁地了(笑)。

此前已经分析过,共享对象有Waitingqueue和Passengers两种:

  1. WaitingQueue: 首先分析有哪些地方会修改或读取waitingqueue对象:InputThread线程会对waitingqueue的乘客列表进行增加操作,且当InputThread结束时会修改waitingqueue的isEnd标志;Scheduler线程会读取乘客列表,也会读取isEnd标志的状态;Elevator线程也会读取isEnd的状态。然后在每个涉及到以上操作的地方加锁即可,我是直接用synchronized(WaitingQueue)锁住了waitingqueue对象。
  2. Passengers: 首先Scheduler线程在调度时会对其进行增加乘客操作,然后电梯在完成一个请求时,需要执行删除乘客操作,最后电梯在判断结束条件时,需要获取乘客列表判断是否为空。但是又有一个问题,每个电梯需要加不同的锁,那么调度器如何获取每个电梯的锁呢?因此我封装了PassengerQueue类,保存有每个电梯的Passengers类,并传入调度器,调取器只需按序号取出并加锁即可。
关于等待与唤醒

每个线程显然是不能一直运行的,否则会大量消耗CPU资源,因此必须为每个线程设置好等待与被唤醒的时机。

  1. InputThread:等待下一个输入时,线程会自然等待,CPU是不消耗的,无需额外使用wait。此外当输入结束时,需要唤醒waitingqueue锁,唤醒调度器进行下一步动作。
  2. Scheduler:我的策略是每一个循环都无条件等待,等待别的线程来唤醒,因为等待队列中的乘客不一定每次都能全部分配掉,如果设置等待条件的话可能会造成调度器无限循环,消耗CPU资源。然后当每个循环分配完毕后,需要唤醒所有电梯的Passengers锁,叫醒那些可能正在wait状态的电梯。同样的,在线程结束前需要唤醒所有电梯,以便正在等待的电梯直接结束线程,不会陷入无限等待。
  3. Elevator: 在每个行动前,先判断当前电梯是否为空,如果没有乘客当然就直接等待,然后根据模式的不同选择恰当的唤醒调度器的时机,Random模式中每行动一次唤醒一次即可,而Night和Morning模式中可以在每次到达1层时唤醒,节省调度器的无意义运行。

Bug与互测情况

第一次作业我存在有死锁的问题,调度器与电梯的线程中相反的嵌套了waitingqueue锁和passenger锁,数据一多就很容易造成死锁。中测数据强度太低导致我没有发现,结果搞得强测全挂没进互测,泪目。

第二次作业

作业要求

果然本次作业初始的电梯增加到了3个,此外还可以最多新增两部电梯,没有其他的要求。但好在我第一次作业就考虑了多电梯的情况,所以我第二次作业其实写的很快。

思路分析

听取了课堂上老师的意见以后,我决定为每个电梯额外增加一个waitingToPickup的容器,顾名思义,用于储存电梯要去接,但还没有接到的乘客,之前的Passengers用来储存当前已经在电梯里面的乘客。这样的好处调度器和电梯的逻辑可以极大的简化,大幅精简了我的代码量,但是整体架构还是没有变化

简化之前,电梯只保存有当前在电梯里的乘客,因此调度器进行分配时,不仅需要判断电梯是否能捎带当前的乘客,还需要需要判断电梯当前是否能开门,此外还涉及到主请求的设置的问题,若当前电梯没有乘客,那么去接第一个乘客时,需要调度器额外将电梯的目标设置为第一个请求的当前楼层,而这个主请求又不好放哪里,只能给电梯再加一个属性,搞得很麻烦。
而采用了waitingToPickup容器后,电梯和调度器的分工一下子就明确了,调度器只负责调配入waitingToPickup队列,电梯只负责处理主请求的接送,送完就在waitingToPickup找下一个主请求,还没有就等待。这样一来调度器的任务大大简化,调度器与各电梯之间的交互也减少了很多,逻辑简单而清晰,看着很需服。

在共享块的加锁与唤醒上与第一次作业基本一致。

bug与互测情况

这次作业运气还行,没有什么bug,强测全过,互测也没被hack到。
但是这次作业忙着冯如杯我也没去找其他人的bug,下次一定(雾)。

第三次作业

作业要求

电梯分为了三种型号A,B,C型,每种电梯的参数与能够到达的层数都不同,可能需要换乘,很考验策略的一次作业。
A:最慢,但是能到达所有层。
B:中等,但是只能到达奇数层。
C:最快,但是只能在1,2,3,18,19,20层开门。

思路分析

其实之前就知道第三次作业需要换乘,根据往年的题目也仔细思考了一些方法。但是题目出来以后发现这次作业的题目与往年其实差别不小,三种型号的电梯不再是分别接送三个区间段的楼层,而是电梯能到达大部分楼层,但有运行速度的差异。

思来想去,其实也没有什么好的换乘办法,假设有乘客从1楼到16楼,那么用B或C电梯将其先送往17或18层,再由A电梯去接下来或许是个办法,但是又如何判断当前A电梯在不在附近呢?若电梯此时就在1层,那岂不是更耗费时间。或许可以进行动态的判断是否需要换乘,但是需要判断的地方,需要判断的情况也实在太多了,就算整出来感觉也不太优雅。与很多同学讨论过之后,发现直接用蠢办法省时又省力,效率甚至还不低。

两头的乘客去C电梯,奇数层的乘客去B电梯,偶数层的去A电梯,简单又优雅,好耶!(雾)

唉,话说回来,用这种方法也是无奈之举,这次的题目环境下也没有啥比较简洁的换乘策略,又没时间去搞枯燥的打表,还不如直球出击。

使用这种原始办法操作就很简单了,基本没有什么变化,只是修改了遍历电梯的顺序,增加了捎带的条件,前前后后改了也没一两个小时就通过了中测。

bug与互测情况

本来以为用蠢办法分数不会太高,结果一看强测99分,妈耶。到头来最蠢的办法比大部分人还快,真是世事难料(笑)。
互测运气还行,没有被hack到。这次抽了点时间看了下别人的代码,发现想目测找bug简直难如登天,代码难看懂不说,多线程问题也不是简单的看两下测两下就能找到的,和我一个room的人也都基本是强测全过的选手,还有几个hack了几百次的机器人大军。试了几组我踩过的坑以后,无奈放弃。

三次作业的UML图

类图:
image

协作图:
image

心得体会

第二单元作业的设计感比第一单元作业要强很多,不比第一单元作业的憋算法,个人感觉第二次作业要更加的面向对象一些,只要设计得当,就可以很简单明了的解决问题,所带来的成就感也是第一单元作业所无法比拟的,也更好的培养了我的面向对象的思维。多线程之间的交互协作是本单元作业的主题,而如何把握多线程与同步块之间的平衡是本单元作业的一大难点,但是熟练了之后说来其实也很简单,无非就是对于多线程共享的数据需要加锁控制而已。而我个人认为最难的地方其实是把握多线程编程的设计思想与设计方法,哪些地方要使用多线程编程,离开这次作业,我是不是能在其他问题上使用多线程?这次作业我只使用了生产者-消费者模式,而主流的调度模式又何止几种?哪些情景适合用哪种?这些都是需要我们去额外理解掌握的,正如我们OO课荣文戈老师所说的,要找到那种感觉,之前一直觉得这句话说的很玄乎,而现在我已经深以为然了。找到多线程编程的感觉,不如说是要深刻理解多线程的思想,体会其中脉络,在多线程编程中保持良好习惯,这样才能在少犯错的同时,写出优雅简洁的代码。

posted @ 2021-04-23 17:15  kiasama  阅读(98)  评论(1编辑  收藏  举报