面向对象第二单元总结

weixin_41030950 weixin_41030950 墨衍会员 · AI 创作全网分发 墨衍智能分发 通过墨衍一键同步至各平台 可同步平台 CSDN微信公众号微博知乎掘金百家号博客园抖音小红书 你的文章也可以这样分发 墨衍会员 ¥399起/年,写一次发全网 我也要 + 加入社区

面向对象第二单元总结

第二单元的任务为模拟群控电梯调度,重点在于多线程并发的设计。以下是任务的迭代:
· hw5模拟了6部电梯的调度,乘客的请求会指定电梯来接受
· hw6的乘客可以自由分配给任意一部电梯,电梯需要输出已接收信号。同时增加了电梯参数的重置,重置时会暂停并处于静默状态
· hw7增加了分裂为双轿厢的重置模式,需要在换乘层进行协调

多线程设计以及调度策略

hw5

本单元第一次作业中由于指定了接收乘客的电梯,故只需要考虑电梯运行的策略。
这次作业中我使用了教程中提到的ALS方法,即电梯的运行方向由当前的主请求决定,并在路上捎带同向请求
在我的设计中,每个电梯有单独的线程,数据的投喂与调度器同为main线程。数据投喂进调度器的全局等待队列中,再由调度器塞给每个电梯的等待队列,在电梯运行到合适的位置并开门后,会处理等待队列中可以上电梯的请求,将其变为电梯内乘客集合中的一份子。
每个电梯的行为决策都由各自的policy类负责给出,每个电梯线程都始终运行在 获得并更新主请求-询问policy得到下一步动作-执行动作 的循环之中,直到接受到来自外界main线程的终止信号。电梯在开门后处理本层的进出请求,将所有到达层为本层的乘客放出,并将等待队列中所有出发层在本层且同向的乘客放入,前提是电梯未满。
调度器的工作则是将新的请求放入对应电梯的请求队列,并notify该电梯。

本次作业的UML类图与协作图如下所示

img

为使电梯运行效率提高,每部电梯均在输出关门前一刻处理乘客的放出,然后处理乘客的进入。

img

hw6

在本单元第二次作业中,由于允许自由分配请求,以及增加了电梯的重置,需要在调度器和电梯的运行策略上进行更多的考虑。

电梯运行策略:

由于上次作业的ALS算法的性能分实在太低,这次采用look算法,即电梯不再由主请求引导,每次处理完当前全部的同向请求后再调头。
同时,加入一点改进:
由于电梯的乘客只在一段楼层区间内占用电梯容量,因此电梯在没有新请求时,到达每个楼层时空余容量都是不一样的,用全局容量来判断是否满员会造成运力的浪费。例如电梯只剩最后一个名额,但其实可以捎带上1-3层与3-5层的两名乘客。因此,在hw5的基础上新增一个注册队列,位于电梯内乘客集合与电梯等待队列之间。新请求如果可被捎带,则直接放入注册队列。在捎带中判断电梯对一个请求的剩余容量(此时电梯的剩余容量的查询结果与新请求的楼层区间有关)时,会计算电梯内乘客数量加上注册队列中与该请求区段有重合的请求数量之和,来判断电梯对该请求来说是否已满。
因此,注册队列实际上是维护了电梯即将捎带上的乘客集合,并使电梯为他们预留出空间。电梯在进人时也会只考虑注册队列中的请求。

电梯运行的每个周期中,都会依据当前运行方向和剩余容量更新注册队列。

调度器分配策略

为了降低整个系统的复杂度,我选择了一个简单的分配策略,分为两步:

  1. 如果当前请求经上述查询后发现可以被一部电梯同向捎带,则将这个请求直接放入该电梯的注册队列
  2. 如果没有电梯可捎带该请求,则选择等待人数-电梯容量最小的一部电梯,将请求放入其等待队列

由于电梯在接受到请求时可能处于重置中,因此对于RECEIVE的输出应进行异步处理。每个电梯的注册队列和等待队列接受到请求的同时,都会向另一个队列(称为接收队列)填入接收信息,并在电梯合适的的时候由电梯线程输出。

电梯重置操作

电梯需要先将所有乘客放出,所有接受到的请求取消,然后静默1.2s完成重置,因此,电梯在接收到重置请求后的下个动作,即是开门并放出所有乘客。然而,电梯释放的不仅有电梯内乘客请求,还有注册队列和等待队列中所有的请求,这些请求都需要将起始层设为本层后交还给调度器,进行再次分配。

终止条件的判断

由于本次作业中电梯的终止不再仅由分配给自己的请求决定,因为其他电梯重置后的请求也可能分配给自己。因此本次作业采用另一种方式判断终止。

在调度器中新建一个信号量onlineNum,初始时为所有电梯线程的数量,即6。每当电梯空闲时,就将onlineNum减1,并notify主线程,从空闲状态恢复时,再将onlineNum加1。主线程则在处理完所有输入后进入一个循环,监听onlineNum的减少。如果onlineNum为0,则说明所有电梯均处理完了请求,此时再给所有电梯发送终止信号。

hw6的架构与hw5基本一致,调度器增加了分配请求的逻辑,而电梯新增了接受重置请求与执行重置的逻辑。UML类图与协作图如下

img

img

hw7

本单元第三次作业需要在hw6的基础上考虑双轿厢的重置和调度问题。

如何重置为双轿厢

在外界看来,双轿厢重置与hw6的普通重置行为一样,都是取消所有请求并静默。但在电梯(井)内部,需要分裂成两个电梯并移动到换乘层两侧。对于电梯的分裂,为了运行性能,需要分裂成两个线程来分别运行。我的方法是将原电梯变为下层电梯,新增的电梯为上层电梯,从原电梯深克隆而来。两个电梯共享一个id。

双轿厢电梯运行策略

每个轿厢与普通电梯的运行策略基本一致,差别在于双轿厢在到达换乘层后,会放出所有电梯内的乘客,变为新请求交给调度器重新分配,类似普通重置时对乘客请求的处理。

如何避免两个轿厢不碰撞

双轿厢电梯在到达换乘层后的下一步总会移动到本段远离换乘层的一侧,如果刚好处理完所有请求变为空闲状态,则使其仍然运行,直到离开换乘层。这样的策略可使得两个轿厢都最多在换乘层停留一个或几个动作周期,另一侧的轿厢只需等待一会即可。

调度器策略

由于hw6中将调度器与主线程仍在同一个线程中,会造成重分配请求时电梯线程与主线程的耦合程度急剧增加,且主线程需要同时负责接受输入与分配请求至电梯,再次增加了耦合程度。上述种种缺陷在hw6中造成了数不尽的死锁,故在hw7中单开一个调度器线程,只负责接受新请求并分配。该线程也的休眠与工作也会影响到onlineNum。

调度器在分配时,需要考虑到双轿厢电梯。我的策略是,在发现该电梯id是双轿厢电梯后,只从两个轿厢中选取一个能接到该请求的轿厢(如果请求从换乘层起始则由终点层来判断)参与最优分配电梯的比较。这样,配合上面的运行策略,可以使需要跨换乘层的请求通过两次分配,使用同样的分配策略来完成。

hw7的代码架构与hw6相比仍然变化不大,只新增了一个DualPolicy作为Policy类的子类,针对双轿厢重载了一些方法。UML类图与协作图如下所示

img

img

同步块的设置和锁的选择

hw5

由于线程间的交互只存在于调度器将请求放入电梯的等待队列的过程中,因此电梯等待队列是hw5唯一的临界资源,对其的访问均包含在synchronized(waitList)块中。

除此之外,为了防止轮询,电梯在空闲的时候会进入该同步块,并用waitList.wait()来等待新请求的填入,调度器会在填入新请求后notify对应的电梯,使其开始工作。当全部输入结束后,电梯进入空闲状态即终止线程。

hw6

在hw5的基础上,hw6新增了调度器对请求的分配,这个过程中涉及到对电梯状态的查看,这需要与电梯执行动作互斥进行。因此,hw6中的电梯新增一个actionLock互斥锁,每个涉及到电梯状态的改变(如上下楼)都需要进入该锁的同步块内,同时调度器在查看电梯是否能接收新请求时也需要拿到该锁。除此之外,注册队列也需要进行同步控制,但由于涉及到的操作比较单一,因此可以用ConcurrentLinkedDeque来实现线程安全的注册队列。

同时,onlineNum的操作也需要是原子的,因此onlineNum增减的相关方法也需要用一个互斥锁控制。

hw7

在hw6的基础上,发现电梯等待队列在多数临界区内都是进行读取,因此将等待队列改为用读写锁控制访问。除此之外,两个轿厢也需要在进入换乘层前检查该井道换乘层是否被占用,并在进入后标记占有、离开后标记释放。我是用一个boolean的list来维护6个井道换乘层的占用情况,对这个list的访问时完全互斥的。

bug与debug

本单元的三次作业均被hack成功(破大防)

hw5的bug在于依靠上人后统计人数是否达到总容量来决定是否退出处理乘客进入的循环,这样会使得满员后的第一个人没有被限制(7==6为false,程序认为不需要break)。修改满员的判断后通过。

hw6的bug在于上述的耦合程度爆炸造成的无处不在的死锁。在去掉一些互斥访问控制后解决,但这样治标不治本。在hw7中直接将调度器线程重构。死锁bug的发现可以借助java命令行工具jps与jstack,前者可以显示所有java进程的pid,后者可以打印某个pid对于java进程中每个线程的执行情况,多数情况下可以直接显示出发生死锁的线程的调用trace。

hw7的bug在于双轿厢重置过程之中会以普通电梯接受分配,造成重置后的A轿厢无法到达上层接上出发层在上段的乘客。通过在双轿厢重置时也加上actionLock解决。

心得体会

本单元最为折磨的地方在于写出线程安全的程序,如果对于互斥访问的控制加的太多,则会轻易触发死锁,如果加的太少,则会出现并发不安全导致的错误。正确的程序需要平衡上述两个方面。同时,多线程的调试和bug复现也比单线程程序困难,因此更应该熟练掌握各种工具,借助工具来进行调试。

...全文

相关推荐

评论
收藏 已收藏
分享
返回顶部