OO第二单元总结
第二单元总结
一、同步块与锁
线程的同步与锁的应用是多线程中不可避免的问题,那么首先就来谈谈这三次作业中同步的实现位置,以及实现方式。
哪些地方需要用到同步块和锁
电梯总体的逻辑都是大致相同的,无非是乘客输入请求、调度器分发请求、电梯完成请求。之所以多线程要用到同步就是因为有共享数据,多个线程同时读写共享数据可能会引发错误,因此我们首先要找电梯系统中所用到的共享数据。
首先应该想到的是电梯的输入请求队列,我们将其定义为waitQueue
,因为乘客会将请求输入到waitQueue
,而调度器要从waitQueue
拿东西,所以waitQueue
肯定是共享的。然后就是每个电梯自己的等待队列,在我的架构里,由控制器将请求分发给各个电梯,因此电梯的等待队列由控制器和电梯自己共享。
还有一个容易忽视的问题就是输出时的线程安全问题,我开始也很不能理解,既然是输出时才获取时间戳,那先输出的时间戳肯定就小啊,不可能出现时间戳不递增的情况。于是我第一次作业也就没有处理输出线程的安全问题,结果强测也都过了,但是在互测的时候被hack了这个问题。只要输出很集中,我的程序就有小概率出现时间戳不递增的情况。后来看了讨论区大家的讨论我才意识到,输出的过程并不是原子的,可以理解为输出分为两部分,一部分是获取当前时间戳,另一部分是打印到控制台,这两部分是可能被分割的。比如第一条输出刚获取到时间戳,这时候第二条输出插进来了,获取时间戳然后输出,等第二条输出完后再输出第一条,这样时间戳就反了。
如何同步
在我的三次作业中绝大部分同步都采用的是synchronized函数。
我将请求队列定义为一个类RequestQueue
,这个类最重要的方法就是addRequest
和getRequest
,这两个方法都是操作共享数据的,所以我直接将这两个函数设置为synchronized。这样就保证了插入请求和获取请求不可能同时发生。在getRequest
时,如果队列中还没有请求应当wait()
,相应的,在addRequest
和setEnd
时应当notifyAll()
。
在第三次作业中,我在控制器中定义了Passenger
类,用来存储乘客信息。在控制器分配请求和电梯返回请求时都会涉及到操作Passenger
,所以我也给这两种操作加上了synchronized。这里不论是用同步函数还是同步代码块其实都可以,如果Passenger
在函数中出现少的话用同步代码块显然效率更高。
最后别忘了在输出类中添加synchronized。
二、调度器
第一次作业调度器
第一次作业可以几乎忽略调度器这个东西,硬要说的话调度器是被融入输入线程了。在输入的时候根据乘客电梯楼座的不同直接将请求放入对应的队列即可。
第二次作业调度器
第二次作业多了纵向电梯这一个东西,而且电梯也是可添加的,我们就真正需要一个调度器来实现调度的功能了。这个调度器首先要区分两种请求:一种是增加电梯的请求,包括增加横向电梯和纵向电梯,要在调度器中写对应增加电梯的方法;另一种是乘客请求,根据是横向还是纵向分别将乘客放入对应的队列,总体实现还是比较简单。
第三次作业调度器
我觉得第三次作业的调度器才是真正意义上的调度器,之前的两次作业调度器只是将请求进行了分类,以至于只要输入结束,调度器进程关了也没什么事。但是第三次作业不同了,乘客的请求不是只有一段,而可能是多段的,所以我们必须要让电梯也有发请求的能力,乘客在走完一段后,相应的电梯再发请求给调度器,让调度器再安排下一段路。这就要求我们的调度器不是在输入完毕后就可以关掉,而是要让它一直随电梯运行下去,或者单独写个方法。(但我还是认为让调度器一直运行下去更符合实际)
还有和一二次作业很不同的一点就是,乘客的路线是要自己去决定的。这里虽然有诸多优化可以做,但仔细去想实现还是非常有难度的,所以我直接使用了课程组提供的方法,将所有楼层搜索一遍,看从哪层过最短。
至此,路径已经规划好了,那么到底安排哪个电梯给乘客呢?这里我是根据电梯的速度不同,按比例将乘客分给不同的电梯,虽然肯定不是最优的,但实际运行时间还是可以接受的。
采用单例模式
课上老师特别说明让我们都用单例模式去写调度器,这样做确实是有道理的,因为调度器是全局唯一的。当然我们也可以将调度器作为参数传入构造函数,但这样确实显得很麻烦。用单例模式就可以做到要用的时候直接调静态方法。
其他调度思路
在与同学的交流中我了解到,不少的同学根本就没有实现调度器的调度作用,而是直接让电梯去自由竞争,这样看似很混乱,但实际运行效果一点都不比调度过的差。其实我们平时坐电梯就是自由竞争的,谁先来,等到电梯就直接走了,并不需要考虑太多,所以完全让电梯自己去“抢”乘客也不失为一种好方法。
三、线程协同及架构
第一次作业架构
话不多说,直接上类图:
可以看出输入线程和电梯线程之间是通过RequestQueue
来连接的,而RequestQueue
里面实际就是个由单个Request
组成的队列。由于第一次作业比较简单,我直接在主类中创建了输入线程和电梯线程。
第二次作业架构
第二次作业的电梯有两类,方法都大致相同。调度器全局调度各类请求队列,由于电梯可以动态添加,因此主方法中只是创建了输入线程和控制器线程,而电梯线程的创建由控制器完成。
第三次作业架构
第三次作业在控制器中加入了乘客对象,用来保存乘客的行程信息。在运送完乘客后,电梯可以向调度器发请求,让调度器对电梯进行进一步调度。其中调度器中的plan()
是用于静态为乘客分配路线的函数,allocate()
是动态为乘客分配电梯的函数。
第三次作业UML协作图
由主函数创建出输入线程和控制器线程,控制器创建电梯线程。输入线程向控制器传递请求。控制器处理请求后将乘客请求分发给相应的电梯,电梯处理完请求后再将结果返回给控制器。
工作模式
输入线程——请求队列——电梯,构成了生产者-消费者模型。输入线程往托盘(请求队列)添加请求,由控制器将请求分给各类电梯。
电梯采用线程特有存储模式,数据全部属于电梯线程,保证电梯销毁之后数据也会随之销毁。
四、自己出现的bug
- 之前说过的输出线程不安全,在互测中才发现。
- 第一次作业不细心,乘客进出应当是先下后上才不会超载,没有考虑进出顺序,导致强测扣了很多分。
- 第三次作业自己写完之后运行时发现CPU占用率极高,我知道自己肯定出现了轮询。用打印的办法找了半天才查出轮询的原因——
wait()
的条件出错,导致实际上并没有wait()
,而是一直循环。 - 第三次作业强测超时了4组,应该是调度策略的问题,但是在本机上运行并不会超时。
五、hack策略
第二单元如果要自己构造数据的话确实是非常无力,就算构造出来又如何检测。所以在第六七次作业中我都自己写了评测机,真的很好用!
评测机主要分为数据生成、运行程序、检测答案的正确性,其中第三步是最繁琐的。以第三次作业为例,我主要从以下几个方面检查答案:
- 单调性检查:时间戳是否单调不递减
- 电梯速度检查:电梯是否超速
- 电梯逻辑检查:电梯是否出现未开门就关门、未关门就运行等
- 电梯超载检查:电梯是否超载
- 电梯可到达可开门性检查:电梯是否可以到达某层某座、某座是否允许开门
- 乘客到达信息检查:每位乘客是否都到达了目的地
我将同组的代码下载下来,都用评测机跑过一遍。出现较多的问题是,电梯在不该开门的地方开门、电梯运行超时。
六、心得体会
第二单元练习了多线程的使用。对于以前从未用过多线程的我来说可以说是收获巨大,从线程的创建、同步到通信,多线程最基本的模式可以说是在几次作业中用得比较熟练了。
现在回顾起来还是觉得第一次作业最具有挑战性,毕竟从无到有更难。二三次作业就只是一些迭代,难度我认为不大,只是其中遇到的一些问题还是费了很多时间。论架构算法的话我的程序绝对算不上好,这个单元我也没有重构过,于是程序就在一次次迭代中越来越乱,不过还好电梯的逻辑并不复杂,还是看得过去。
多线程的调试确实很让人为难,我自己调试全是加输出语句,至于任务管理器或是jconsole就只能作为辅助判断工具,具体逻辑错误还是得靠sout。
总之这个单元收获许多,不只是多线程,编写评测机也可以说是让自己乐在其中(不至于忘记python,评测机debug或是hack真的好用),此外还了解到了一些常见的工作模式。与第一单元不同的是第二单元感觉更贴近实际了,而不是像表达式那么枯燥,能将所学用于实际确实感觉很好。