2020级北航OO第二单元作业总结
一、总体思路与架构
本单元主要实现的是一个多线程电梯调度程序。其中最重要的是对锁、同步块以及生产者与消费者模型、流水线模型的理解。在做第五次作业的过程中,为了更好地理解整体架构与实现过程,于是,我画了一个比较具象的图(仅对应于我的第五次作业代码,不适用于第六次、第七次作业):
可以从上面的图中看出我在第五次作业分了以下几个类:
·Person:人,对应于官方包中的Request。每个人都有id、FromBuilding、FromFloor、ToBuilding、ToFloor这几个属性。
·InputStream:输入线程类,主要作用是将输入的人搬运到传送带上。
·ConveyorList:传送带类,主要作用是让人们排队。
·Controller:调度器类,主要作用是让对应的人去对应楼座和楼层的电梯的外面等待。
·OuterRequestMap:电梯外部等待类,主要作用是记录电梯外面的需求等待队列。
·InnerRequestMap:电梯内部等待类,主要作用是记录电梯内部的需求等待队列。
·Elevator:电梯类,主要作用是根据乘客的需求将其运送到指定地点。
·Mainclass:主类,用于初始化电梯类、输入类... ...
其中加粗的类是线程Thread,需要实现run方法,倾斜的类是需要加锁的类。
可以看出,如果有多个线程都会对这个类增删(即有人在等待的队列的类),则这些增删操作一定要加锁,我是通过synchronized同步块实现的。下文会详细讲述。
下图是我第七次作业的最终的UML图:
代码复杂度:
从上图中可以看出红色的类还是很多的,主要体现在Controller、Elevator、InputStream这三个类中。
二、第五次作业
2.1 UML图:
其中红色框框出来的部分是这次作业的初始架构。
2.2同步块和锁的设计:
ConveyorList类中的每个方法都加了synchronized关键字,并且十分重要的是:在传送带中加入人时(对应于类中的put方法)以及传送带收到结束信号之后(对应于类中的setDone方法)之后都要notifyAll();否则就会轮询。
同样的,OuterRequestMap中会对等待队列产生影响的方法我都加了synchronized关键字。
除此之外,在Controller和Elevator的类中,也应该要加synchronized关键字,具体情形如下:
synchronized (conveyorList) {
if (conveyorList.getNum() == 0 && conveyorList.isDone()
&& conveyorList.getCount() == 0) { //结束
... ...
break;
} else if (conveyorList.getNum() == 0) {
wait... ...
}
虽然conveyorlist中的方法都加了锁,但是如果没有外层的锁,很可能会导致最终线程结束的时候有一个人没有被送到指定地点。
2.3调度器设计:
在第五次作业中的调度器就是根据需求,将其送到指定楼层和楼座的外面等待即可。甚至不需要加调度器也能够实现。
2.4纵向电梯的Look算法:
我实现的Look算法有如下注意点(可能与真正的Look算法不太一样):
·同方向就捎带,意思是说如果电梯往上运行,如果此时有个人也要向上,就捎带。
·人在进电梯之后,要对目的层进行重置,如果电梯方向向上,则这一层所有人都进来之后,则目的曾应该是所有需求层的最高层,如果现在电梯方向向下,则目的层应该是左右需求层的对低层。
·人在出电梯之后,需要将这个需求从innerRequestMap中移除,同时也要重置一下目的层。
三、第六次作业
3.1同步块和锁的设计:
相比第五次作业增加了横向电梯ElevatorH类,横向电梯的内外需求类InnerMapH,OuterMapH类。其中关于锁的设计和上一次作业几乎无异。
3.2调度器设计:
这一次调度器就起到了作用了:
if (person.getFromBuilding() != person.getToBuilding()) ->乘坐横向电梯 否则乘坐纵向电梯
同样的,根据其所在楼层配送到指定楼层和楼座的电梯外面等待。
3.3横向电梯的Look算法:
和纵向电梯的Look算法相似。但是不同的点在于横向电梯能够循环ABCDE五个楼层地运行。所以如果电梯现在是顺时针运行,则在同方向的最远端(此处同方向的最远端指的是照着这个方向往后数两个,例如A(顺时针),则其最远端是C,反方向的最远端是D,这是因为如果需求在D-E就要改变方向,如果是B-C就说明是同向的,则不需要改变方向。)因此如果需求在同方向的最远端之内,则一直不改变方向地运行下去,否则掉头运行即可。
四、第七次作业
4.1同步块和锁的设计:
这一次作业相比上一次作业多了换乘的要求,这其实是对调度器的一个设计。除此之外,还要在Elevator和ElevatorH两个电梯类对ConveyorList加锁:
synchronized (conveyorList) {
conveyorList.putFirst(person);
conveyorList.subCount();
}
我对换乘的设计是:例如A-2-TO-C-3,先将其运送到第三层,然后再new Person,这个Person的需求是将A-3-TO-C-3,再将这个Person插入到传送带队列的首位,
让调度器分配给指定的电梯运行,依此类推,就能实现将换乘的人运送到指定楼座和楼层了。
4.2调度器设计:
我认为我此次作业的调度器实现得相对比较复杂。我的思路是:
·FromFloor和ToFloor两层有没有能够抵达的横向电梯(此处我用了一个canGet函数判断),如果在目的曾和起始层其一存在一个横向电梯的话就说明是需要换乘一次,否则就只能在一层或者别的更近的楼层换乘,那就会换乘两次。
·对于要去一层或者别的更近的楼层换乘的实现思路是,首先在ToFloor和FromFloor之间寻找有没有能够抵达的横向电梯,如果没有则找利toFloor或者是FromFloor最近的能够抵达的横向电梯。这样能够实现最多两次换乘,但是无法实现多次换乘。多次换乘确实能够利用更多的电梯,理论上来说更加节省时间,但是多次换乘也意味着多次等待,如果数据集小,可能多次换乘的优势不是很大,但是数据集大的话,多次换乘的实现会更具优势。
4.3流水线模型的实现:
流水线模型的关键在于有一个计数器。在这次作业中我没有用的典型的流水线模型,而是将处理到一半的数据重新放到对头等待调度。不过我在ConveyorList中也设置了一个计数器,用于记录的是每个人的换成次数总和。主要作用是用于判断进程有没有结束。因为如果没有这个计数器的存在,很可能会出现内外队列都为空,结束信号到来进程就结束了,但是还有人只送到了一半,并未到达指定楼座和楼层的情况。
五、自身程序Bug
1、在if语句中wait(错误)-> 应该是在while语句中wait
这会导致最后一个乘客无法到达指定楼层和楼座,因为wait之后不会重新判断条件导致实际上结束信号已经发送了,但是wait之后没有重新判断从而还无法结束。
2、第七次作业没有考虑到同层换乘且要去一楼换乘的情况
这种情况最开始没有考虑到,是在第七次作业互测的时候被Hack了,在强测中似乎也没有这种数据orz,这种情况由于我没有将conveyorlist中的计数器加1,导致这个人永远到不了指定楼层和楼座。
3、输出线程不安全 -> 封装输出类
这是在第五次作业互测的时候发现的,是因为没有封装输出类,导致输出时间不递增的情况。
六、Hack
hack的方法主要有:卡TLE(最长运行时间是70s,在六十几秒的时候添加需求)、对横向电梯的需求增加复杂度(看看会不会出现一只循环或者轮询等情况)、一些自己测时遇到的数据点(例如要多次换乘的情况等等)
七、心得体会
这单元的作业让我深深体会到了多线程实现过程相比单线程需要考虑的东西很多,比如何时加锁,何时notifyAll,何时wait以及什么条件下才能结束进程。中途也遇到了很多bug,而且多线程相比单线程debug的难度增加了,需要结合不同线程观察哪一步出错了,所以花费的时间更久。除此之外,多线程最难处理还是各个线程之间的协调,以及需要多了解一些模型才能更完备地完成一些实际的多线程任务。
如有错误请指正!