OO第二单元总结

OO第二单元总结

(一)同步块的设置和锁的选择

  • 第一次作业

本次作业实现一部电梯运送乘客,需要同步的部分就是乘客队列,因为输入要把新的乘客放入队列,电梯要把乘客从队列中取出,这些功能在我的BufferHole类中实现。

BufferHole中使用了java的线程安全类LinkedBlockingQueue,保证了对队列操作的线程安全,类中的所有方法,如put,getPersonAtFloor,empty等都声明成了synchronized方法,也就是对调用方法的BufferHole实例上锁,保证在同一时刻只有一个线程在使用BUfferHole又因为架构中只实例化了一个BufferHole实例,所以保证了线程安全。

  • 第二次作业

第二次作业和第一次作业相比,增加了电梯数量和动态增加电梯的请求,我的设计中,每部电梯拥有自己的缓冲区,乘客通过调度器被放入指定电梯的缓冲区,这保证了每部电梯的线程安全,而且电梯之间互相不可见,也不会产生对乘客的抢夺问题,也就是采用投入乘客到电梯而不是让电梯取乘客的方式,从而保证了线程安全。

增加了AllRequest类存放所有的请求。

另外,因为官方包的输出并不是synchronized,因此自己定义了一个Out类,把官方包的输出用一个synchronized方法"包裹起来",从而保证了输出的线程安全,也就是说,因为输出这个操作不是一个原子操作,如果不这样做可能导致一部电梯取得的时间戳和另一部电梯的后半部分输出被拼接起来输出,导致输出的时序混乱。

  • 第三次作业

第三次作业设定了电梯的类型,每种类型只能在某些固定的楼层停靠,这表面上只需要改调度算法,但是对我的架构来说,这也引起了新的线程安全问题。

因为输入不再只来自标准输入了,可能来自换乘,同时为了让电梯停下我设计了两个特殊请求:标准输入结束,当前所有电梯和缓冲区都空,显然当标准输入结束后,如果所有电梯和缓冲区都是空的,那么说明调度结束,电梯要停止工作,因为这一判断应该发生在某个乘客从电梯中走出来之后,而一个乘客走出来之后有有可能重新被放回AllRequest,因此把consumer类中的addPerson,addElevator,allElevatorEmpty方法和AllRequest类中的empty方法声明为synchronized方法,保证了判断是否应该投放AllElevatorEmptyRequest这一请求的逻辑是线程安全的。

(二)调度器设计

  • 第一次作业

第一次作业中,我并没有使用调度器,而是直接使用了look算法,同时进行了一些优化。

BufferHole中,每层有一个请求队列,队列用java的线程安全类LinkedBlockingQueue存放。

标准的look算法就是电梯往一个方向移动,只有同方向请求才会被放入电梯,然后电梯空并且当前方向上(也就是当前楼层向上/下,这取决于目前的运动方向)没有请求了就会转向,直到处理完所有的请求。

第一次作业时,我认为,不一定同方向的请求才能被放入电梯,因为电梯如果是空的,多放一些人进去说不定可以减少开门的次数,但是这样带来了新的bug,也就是说电梯的转弯策略要变得复杂一些,也就是说当电梯里没有人去往当前方向,并且不满足当前方向上有人并且电梯未满的条件时,就要转向。

后面两次作业中考虑到人数可能很多,还是只放入同向请求比较合理,也把BufferHole中的请求队列改成了每层一个上行请求队列,一个下行请求队列。

  • 第二次作业

第二次作业和第一次作业相比,增加了电梯数量和动态增加电梯的请求,在我的架构中,每个电梯有自己的BufferHole(缓冲区),调度器通过运算把乘客放到相应的电梯的缓冲区里,缓冲区的设计和第一次作业完全相同,保证只有一个输入(标准输入),在调度器和电梯之间增加了一个Consumer类,持有所有的电梯实例,通过计算来确定把乘客投放到哪个电梯的缓冲区里。

每一部电梯还是采用的look算法,也就是因为电梯互相间不可见,可以理解为我设计了很多部独立的look电梯,每部电梯有自己的请求队列,而全部请求怎么分配是调度器的工作。

调度器采用了一个简单的贪心策略,注意到,如果一个乘客是可以被捎带的那么可以比较快的被送到目的地,那么这个可以捎带是怎么判断的呢,Consumer类中的eleCanTakeIt方法实现了这一判断,判断都有哪些电梯可以捎带,可以稍带也就是说,电梯的运动方向和请求方向相同,并且电梯还没有到达这一请求的出发楼层(如果请求向上,电梯在请求以下的层数),此处本应判断到达这一层的期望人数(即hopeNum大小,计算方法见下文)小于满载人数,但后续经过测试,即使人是满载的,把这个请求放到一部同向的电梯里也是比较优的选择,因此不再判断期望人数,随后从这些可以捎带的电梯中选出到达这一层的时候hopeNum最小的那一部电梯,把当前请求放入那部电梯。

如果没有电梯可以捎带那么求所有电梯到达这一层的hopeNum,取最小的那一部电梯,把乘客放入那部电梯,对于hopeNum的计算,考虑电梯当前所在的层数,num的初始值为当前电梯内乘客数,然后减去所有会在当前层到改请求出发层之间下电梯的人,加上这一区间缓冲区内的人数,得到的即为求得的期望人数,这是一个近似值,但是作为一种调度策略,近似的计算也可以达到调度的目的,总体性能也不会有很大差距。

  • 第三次作业

本次作业因为考虑到换乘因此Passenger类设置了三个变量fromFloor,toFloor,toFloorFinal,分别代表出发楼层,本次乘坐电梯要到达的楼层,最终目的楼层,当toFloor == toFloorFinal时,代表乘客到达目的地了,否则需要重新被调度选择乘坐哪部电梯。

第三次作业的电梯A类可以在所有楼层停靠,B类可以在奇数楼层停靠,C类可以在1-3,18-20停靠,而且每类电梯的载客数量,移动速度不同,而且可以停靠的楼层越少,速度越快,载客量也相应越少,开始时考虑的是采用静态的Dijkstra(最后的提交代码中还留着相关部分,虽然没有调用),也就是在不考虑当前电梯状态的情况下,计算从i层到j层的最短时间(只考虑电梯移动时间)和相应的路径,这个路径应该是确定的,那么一个乘客的乘坐路径也就被唯一确定出来了,但是经过考虑,这样可能会让某一时刻某些电梯的负载过大,而且频繁的换乘也可能导致效率降低。

因此最后选择了和第二次作业类似的贪心算法,eleCanTakeIt来寻找可以捎带的电梯,可以捎带的判断做出了一些修改,除了第二次作业中提到的电梯的运动方向和请求方向相同,并且电梯还没有到达这一请求的出发楼层,还应该要求电梯在请求的出发楼层可以开门,而且电梯在请求的出发楼层的下一个可以开门的楼层要在出发楼层和最终目的楼层之间。这样找到所有可以捎带的电梯,然后遍历一次,如果当前电梯比已经得到的最优解的hopeNum小或者比已经得到的最优解的hopeTime小,也就是说,如果这部电梯在人数或者时间上有一个方面更优秀,就选择这部电梯,这样就可以选出该乘客要选择的电梯,把请求放入那部电梯的缓冲区里面。

如果没有可以捎带的电梯,那么使用eleCanTakeItSlowly选出“晚些时候可以运送这一乘客的电梯”,即要求电梯在请求的出发楼层可以开门,而且电梯在请求的出发楼层的下一个可以开门的楼层要在出发楼层和最终目的楼层之间。,然后从这些电梯里选出电梯内和缓冲区内人数之和最少的那部电梯,把请求放入那部电梯的缓冲区里面。

(三)UML类图和UML协作图

  • UML类图

  • UML协作图

主要考虑主线程和调度器线程两个线程和其他线程之间的协作关系

Main线程和其他线程的协作图

Scheduler线程和其他线程的协作图

(四)自己程序的bug

  • 第一次作业

第一次作业的bug出在Elevator的开门逻辑和转向逻辑,开门逻辑没有考虑电梯已经满载的情况,也就是说如果这一层有乘客,即使电梯已经满载而且没有人要下电梯,电梯也会进行一次开关门,同时转向逻辑也没有考虑满载的情况,即在不满足同方向上有请求且没有满载的情况下就要转弯,但没有考虑满载,这两个问题导致第一次作业强测中有4个点超时。

  • 第二次作业

第二次作业的Consumer的调度策略在写代码的过程中出现了两个错误,首先hopeNum的计算中inNum应该是从电梯当前层到函数的传入参数floor所代表的层的缓冲区人数之和,设定循环变量i,循环中增加的应该是第i层的缓冲区人数,但却写成了增加floor层的缓冲区人数;然后是电梯的选择,当有可以捎带的电梯的时候,应该记录选择的电梯在所有电梯中的下标,但是我记录了在可以捎带的电梯中的下标,然后从所有电梯中取出这个下标对应的电梯,这导致了如果只有一部电梯可以捎带,那么请求会一直分配给第一部电梯,这导致第二次作业强测中有1个点超时

  • 第三次作业

第三次作业强测和互测中未出现bug

(五)发现别人程序bug所采用的策略

第二单元作业发现别人的bug仍然是在本地采用测评机测试一个互测屋内所有人的代码,数据根据作业要求的格式随机生成,测评机和数据生成的代码采用python实现

(六)心得体会

第二单元主要学习了多线程的问题,其中最重要的部分就是同步和线程安全,也就是说如果某些变量需要被多个线程同时访问,需要同步这些变量,简单的说就是需要让多个线程访问到同一个那个变量,这同时引出了线程安全的问题,因为有些数据可以被多个线程读/写,如果这些线程的读/写操作同时进行,就可能会有线程安全问题,因为多个线程不知道谁先运行谁后运行,可能倒是结果出错,解决线程安全问题 的一个方法是使用同步代码块/同步方法给对象上锁,通过这种方法,可以保证同一时刻只有一个线程会使用某些代码,这保证不会有多个线程在同一时刻抢着去使用同一个变量从而导致结果不确定引发错误。通过对多线程的了解,在学习新的知识的同时,也提高了我的学习能力。

posted @ 2021-04-27 18:33  _start6417  阅读(90)  评论(0)    收藏  举报