(1)总结分析三次作业中同步块的设置和锁的选择
第一次作业
根据老师在课上的讲述,我选择了生产者消费者模式,同步块采用了指导书中提示的侯乘表类,加锁的选择为生产者消费者模式中的加锁。
第二次作业
第二次作业中我的想法比较简单,就是所有电梯共有一个侯乘表,有每部电梯自己运行,而没有一个专门的调度器。
由于没有调度器,这次作业的线程同步让我极其痛苦。
同步块依然是第一次作业的侯乘表类,锁的选择非常凌乱。
首先是侯乘表类WaitingList中put和get都加了锁,其次是输入线程中错误的加锁
这个错误也导致了我强测挂了两个点。
然后是电梯列表ElevatorList类从WaitingList中获取请求的put方法(与WaitingList中不是一个东西)对waitingList这个对象加了锁。
最后是策略类Strategy中同样对waitingList这个共享对象加了锁。
第三次作业
第三次作业直接重构了。
同步块是输入线程的每个分支判断,和调度器中对共享对象waitingList的状态的判断(是否为空)。
锁的选择也很简单,只有对waitingList的更改和查询才加锁。
(2)总结分析三次作业中的调度器设计,并分析调度器如何与程序中的线程进行交互
我的前两次作业都没有使用调度器,只有电梯在共享对象侯乘表类WaitingList中实施“抢人”。
而第三次作业我直接选择了重构,因为我发现继续使用第二次作业的架构不能很好的完成第三次作业的要求,而且第二次作业遗留了很多的问题,架构有问题,同步块的设置位置太大,程序很慢,而且锁的选择和notify的位置都有问题,因此我选择了重构
第三次作业加了调度器,将每个电梯作为一个线程,电梯中增加了电梯外队列和电梯内队列。电梯外队列相当于前两次作业的WaitingList,这样可以只在外部的调度器中将请求加入每个电梯的电梯外队列,而每个电梯各自的运行则只与自身的属性(运行速度、容量)有关。
交互方法:
输入线程InputProducer
输入进程InputProducer根据输入的请求进行分支,如果是null退出while(true)循环,如果是乘客请求,将其加入共享对象waitingList,如果是增加电梯请求则创建新的电梯线程,start并加入调度器。除了增加电梯,都要进行notify。
调度器线程Schedule
调度器线程Schedule的构造函数中创建了三个电梯线程并设置了id和type。
run方法对共享对象waitingList加锁,因为有对共享对象的读和写,如果waitingList为空并且输入结束,退出while循环,如果waitingList为空输入未结束,则wait,等待输入线程的notify,之后将waitingList的第一个乘客请求加入某个电梯并移除。
电梯线程Elevator
第三次作业我将电梯类作为了一个线程,在电梯线程中,因为我对比前两次作业新增了电梯外队列,调度器把乘客请求加入电梯外队列,因此,只有run方法中的判断waitingList是否为空加了锁,这样可以使电梯线程在只有waitingList为空时wait,而对电梯外队列和电梯内队列,电梯线程的运行相对独立,和第一次作业类似。
(3)从功能设计与性能设计的平衡方面,分析和总结自己第三次作业架构设计的可扩展性
UML类图
画UML协作图(sequence diagram)来展示线程之间的协作关系(别忘记主线程)作业
线程间的协作关系:
在主类Mainclass中创建调度线程Schedule、输入线程InputProducer、共享对象WaitingList,由调度线程Schedule创建每个电梯线程Elevator。如果有增加电梯的请求,则在输入线程中将新的电梯线程创建并加入调度器线程的调度队列中。
功能设计与性能设计的平衡
我的设计完全是以功能的正确性为前提进行的(虽然强测挂了一个点),而且第三次作业是重构的,与前两次作业的关系并不大,只复用了第一次单电梯的运行策略。
为了有可能进行的性能优化,我在WaitingList类中增加了很多判断状态和选择楼层的方法,并且对官方包中的乘客请求PersonRequest重新进行了封装成为PassengerRequest,增加了换乘楼层和第一、第二次电梯编号的属性,但是经过测试发现增加换乘后大概率会让总运行时间变慢,并且在同步块和加锁等问题的逻辑很复杂,很可能在强测中出现很多bug,因此最后的提交并没有选择有换乘的版本,而是最初的没有换乘的版本,只在最开始WaitingList将请求放入各个电梯的电梯外队列时进行选择(优先选择C类型的电梯,优先选择同一类型的请求总量少的电梯),这样可以在保证功能正确性的前提下优化很少的性能。
第三次作业的可扩展性
因为第三次作业是电梯这一单元的最后一次作业,而且由于我并不能够在前两次作业的基础上进行扩展,写出第三次的代码,因此我并没有考虑这次作业的可扩展性。
但在写博客单元总结作业时,再次读自己的代码,却发现自己的代码可扩展性并不是很差。
如果增加电梯的种类
可以直接通过电梯的构造方法对电梯的属性进行分支,也可以通过继承或工厂方法使电梯的种类更加多样。
如果增加电梯可能发生故障(所有电梯内的人在某一层out)
可以在电梯类中将电梯内请求在某一层排出,使用这次作业中未使用的带换乘的请求来表示乘客在故障楼层的out,并且把电梯外请求队列的人重新放回WaitingList。
可见,即使没有考虑可扩展性,但由于这两个单元OO课程的痛苦修炼,我还是基本有了一丢丢面向对象的思维,并且会下意识的考虑代码的可扩展性。
而前两次作业的可扩展性及其低,我认为是对多线程编程(尤其是锁的设置)的不熟悉导致的。
(4)分析自己程序的bug
第五次作业
弱测、中测、互测和强测中均无bug
第六次作业
由于我的第六次作业的InputProducer类中的输入线程的锁加的位置不对,原子性操作会为从输入到把输入的请求传入等待队列,这样会导致即使notify了其他所有线程,但由于原子性操作中request在等待nextRequest时占有锁,无法将锁释放,因此会导致所有输入结束知道输入null才开始运行程序。
因此在强测中挂了两个点。
第七次作业
在第七次作业中,我改正了第六次作业的InputProducer中加锁的问题,但我未考虑到最后一条可能是加电梯的请求,而我的输入线程中只有输入null才会退出while(true)循环并将固态布尔变量isEnd(输入是否结束)置为true。
由于这个错误,我的程序在完成运人之后无法结束,导致最后强测有一个点RTLE。
(5)分析自己发现别人程序bug所采用的策略
只有第一次作业我试图去hack其他人,但是由于我无法在自己的程序中找到bug,理所当然的也并不能很容易找出其他人的代码产生的bug。尤其是在本地测试出bug而提交几次后都没hack成功,我更加质疑自己找到的bug是不是自己输入的问题。
而后两次强测我都没有全部通过,却没有在互测中被成功hack(也没有hack其他人),我认为这和自己在设计程序时考虑不周是有很大关系的。以第三次的强测为例,我没有考虑到最后一个输入为加电梯的请求,因此我的输入线程只有在最后一个请求为乘客请求,之后再输入null(CTRL+D)才能把输入结束标志置为true,导致程序无法正确退出,RTLE,如果我在设计时就考虑到这一点,是绝对不可能出现这种错误的。
(6) 心得体会
多线程的问题
这三次作业的代码写得都很困难,基本都是快要截止时间才勉强把一个还凑合的版本提交上去。多线程编程的困难不仅是在写程序的过程中,需要考虑到共享对象的同步问题、锁的设置,更麻烦的是bug的产生无法复现,偶尔能够复现的bug却很难找到错在哪里,找到错误的点妄图更改,却发现会引起一系列的bug。。。
我的第二次作业强测挂了三个点就是由于当时对多线程编程的理解很差,当时我的程序可以正确运行,但是只有在最后输入null之后所有的电梯才开始运行。我看着自己的程序,读了很多遍,从周三找到周五才发现是因为我是每一层都要进行一次侯乘表类是否为空的判断,这个判断对共享变量waitingList加锁,而释放锁之后,输入线程中的锁加在了while(true)整个循环体的内部(即elevatorInput.nextRequest();也在同步块中),电梯线程偶尔能跑一层或者两层,大部分时间都卡在了等待输入的时间。一直到提交截止我也没搞懂为什么这里会出错。
而在第三次作业中,我认真的读了多线程这部分的课件,发现是输入线程的elevatorInput.nextRequest()在等待输入时占有了锁,而只需要简单的把锁放在后面分支判断中就可以避免这个情况。
关于重构
!!!!一定要及时重构!!!!
第一次作业由于对多线程的理解很差,写出来的代码即使能够运行,但是可扩展性太差。
而第二次作业,我在是否要重构这个问题上纠结了很久,不重构要忍受第一次写的屎山,而重构又担心自己无法在要求的时间把作业写完。最后选择了把第一次的屎山包住,最后的结果很惨烈,强测挂了三个点,都是第一次遗留的问题。
第三次作业时,我觉得我再使用前两次作业的代码可能要死在强测,于是便毅然决然选择了重构。当然我重构又不可能凭空写出和前两次不一样的东西,只能在各个博客找有关多线程编程的博文,并且向其他同学请教他们的架构,最后把多线程的内容搞懂,并且把架构的逻辑在notability中写清楚,写代码竟然只用了周六的一个上午,虽然性能还是很垃圾,但对于我这个多线程的白痴来讲,没有bug已经很满足了。
所以我对自己的叮嘱,一定要及时重构,及时重构能避免强测的很多问题,并且自己在debug时不会对之前的代码一次又一次的感到恶心,甚至觉得自己根本不应该学计算机。。。