前言:

  第二单元总共包括三次电梯调度作业。这三次作业在笔者看来是为了让学生了解什么是多线程,多线程的好处及可能存在的潜在问题,对于多线程的安全问题应该如何解决和保证结果的唯一性和正确性。那么接下来笔者将结合三次电梯调度作业来谈谈在这三次作业中我都收获了哪些。

 

第五次作业:

  结构分析:

  

  

  

  代码分析:

  第五次作业相对来说结构比较简单:Main方法调用静态对象调度器,然后作为参数传入输入和电梯实例出来的两个对象中,最后开启这两个线程。唯一需要注意的就是对调度器中任务链表进行操作的时候一定要记得加锁,也就是在同一时刻只有一个线程能够访问到这个链表,确保不会因为多线程而导致结果错误。这一次采用的调度方式是服务最先到来的指令,好处在于代码容易实现,缺点在于效率低下。

 

第六次作业:

  结构分析:

  

  

  

  代码分析:

    第六次作业相对来说要“智能”一些,因为需要使用一定的调度策略,防止总运行时间过长。此时笔者采用的策略是当电梯没有运转的时候从指令链表中读一条指令,并不取出,根据这一条指令确定一个目标楼层,也就是电梯接下来要运行到那个楼层,那么因此也就确定了一个运行方向。确定了之后就开始运作,并在运行过程中每一层都进行一次遍历(包括出发楼层):是否有同方向且在本楼层上电梯的人,因为还没有考虑载客量,因此一旦有这样的需求,那么就捎带,更新目标楼层,继续运行。

  在写代码的时候笔者便意识到一个问题,就是如果有个人要从15楼到14楼,但是在电梯运行的过程中不断有人要从1楼到2楼,2楼到1楼,那么如果只考虑性能应该是先将1楼2楼的需求处理完之后再去15楼处理第一个请求效率是最高的。但是笔者认为这样的电梯十分“鬼畜”,很容易造成饥饿——即15楼层的请求迟迟得不到处理,然而明明这个请求是最先到来的。笔者觉得这个从实际的角度考虑并不符合实际,所以并没有为了性能分去优化这个地方,而是遵从现实。一味的迎合性能分,我觉得并不一定是一件好的事情。但是笔者的调度策略也并不完全符合现实。假设电梯现在上行,在电梯运行到目标楼层之后,应该判断目标楼层上方是否还有请求,处理完目标楼层之上的请求之后才应该反向,但是我在运行到目的楼层后仍然重复了最开始的思路,从指令链表中读一条指令重新确定运行方向和目标楼层,我觉得这个点是一个值得优化的地方。

  当然上述看法仅代表笔者的观点,每个人的代码思路和原则并不一样,尊重每一个人的想法。

 

第七次作业:

  结构分析:

  

  

  

  

 

  代码分析:

  第七次作业最难的地方笔者认为是调度策略。笔者所用的方法是,将23个楼层设定每个楼层的“归属”,比如1楼的归属为ABC,3楼的归属为C,对于A电梯利用二进制的方式表示为100B,B电梯为010B,C电梯为001B。设定一个类,构建一个私有静态对象,这个对象可以通过方法确定给定的两个楼层是否有相同的归属,很显然如果有相同的归属那么使用归属电梯即可以一步到位。如果没有相同的归属那么就需要换乘,笔者的方法是寻找换乘楼层:顺着该指令运行方向寻找这样的一个楼层:和起始楼层和终止楼层都有至少一个相同的归属,那么这个楼层便可以作为换乘楼层。得到换乘楼层之后就可以把原来的指令变成两个指令进行执行。然后每一条指令都确定了一个电梯来执行,将它放在对应的电梯指令链表中即可。

  有几个特殊的点需要考虑,第一个就是如果是4楼到3楼那么根据这种方式找到的换乘楼层是1楼,但是实际上5楼更加合适,同理,2楼到3楼寻找到的是5楼,但其实是1楼更加合适。这两种情况需要特判一下。还有就是诸如1楼到15楼这样有多部电梯都可以直接到达的情况下,应该怎么取舍。笔者所用的方法是,如果人员已经进入电梯,那么就会把这条指令从调度器对应链表中删除。对于有多种电梯可以选择的情况我们会优先选择目前链表长度最短的电梯,如果链表长度相同那么就优先选择A电梯,再选择B电梯,最后选择C电梯。之所以这么考虑是根据运行的速度。不过后来在讨论课听别的同学的分析,优先级改为A>C>B应该平均效率会更高一些。仔细分析可能和C能够到达的楼层有限,那么很多时候A和B都处于负载的状态而C并没有需要执行的指令或者能够执行的指令并不多,那么有些优先给C便可以分担A和B电梯的压力,整体来看缩短等待时间和运行时间。还有就是如果将指令分割成两个指令之后就有一个顺序问题:一定是前一条指令执行完成后才可以执行后一条指令。对于这个问题笔者使用一个和指令链表同步插入删除的阻塞状态链表。对于普通的指令阻塞状态设为0,对于拆分的指令,前一条指令设为1,后一条指令设为-1。那么很容易知道,我们取指令的时候只能去阻塞状态为0和1的指令。那么-1的指令什么时候结束阻塞呢?一旦有人从电梯中出去,我们就根据他的ID在三个电梯指令链表中查询是否有相同ID的指令,因为如果是普通指令,那么一个ID对应的就只有一条指令,这个时候就不会在三个电梯指令链表中查询到此ID,但是对于拆分的指令,利用前一个指令的ID便可以查到当前正被阻塞的指令,然后将其阻塞状态改为0,这条指令便可以像普通指令一样被取出了。

  解决了策略问题其实还有一些小的问题,比如笔者使用的是while(arraylist.size == 0){wait()}加notifyall()的方式。但是对于这次作业会出现一个问题,就是“轮询”。虽然wait()加notifyall()正常情况下是不会出现轮询的,但是如果出现一条3-16楼的指令时,拆分后就有一条15-16的不可取的指令。那么A电梯中arraylist.size() == 0这个条件就不会满足了,就不会进入循环进行wait(),造成了轮询的情况出现,导致CPU时间过高。解决此办法也比较容易。如果取不出指令时便返回null,如果是取出来的指令是null且不是我们手动输入的结束符号的null,那么便睡眠等待100ms再尝试去取。谈到null,笔者的线程结束方式也比较简单:当输入的是null的时候,将null插入到三个链表中,然后如果取到的是null且当前链表长度为1的时候证明是时候结束此电梯了,置符号endSign == 1,在每一个循环中判断,如果endSign == 1,变返回调用它的方法或终止循环。这样可以确保电梯不会提前下班,同时可以正常结束。

关于SOLID原则:

  

  SRP:有了第一单元的锻炼已经初步掌握了OO的思想,在本单元也基本能符合SRP。

  OCP:基本了解哪些需要设为private,哪些设为public。 

  LSP:三次作业中都没有使用继承,因此本原则没有体现。

  DIP:三次作业中都没有使用继承和抽象类,因此本原则没有体现。

  LSP:三次作业中都没有使用接口,因此本原则没有体现。

 

BUG分析:

  本单元强测和互测中都没有出现bug,在写的过程中出现的bug也已经在上面代码分析中列出并给出对应的解决办法。

 

分析别人BUG时运用的策略:

  首先搭建评测机,随着作业的推进评测机的重要性逐渐凸显。然后测试一些自己再写代码时特别注意的点,或者说自己差一点遗漏的细节,那么别人也可能在这个地方出现问题,那么针对这些问题构建测试数据,查找他人bug。不过不同于第一单元的作业:第一单元的作业很多时候是抓输入输出的错误,而第二单元由于实现了输入输出接口统一化,所查的bug多是逻辑思考上可能出现遗漏的地方。

 

心得体会:

  最开始的时候可能受上机实验的影响,对于不确定的地方全部加锁,这种解决办法也被调侃道“遇事不决就加锁,锁成单线程”。如果是为了解决作业问题这种方法基本是有效的,但是既然本单元就是为了让我们对多线程和线程安全有比较熟悉的掌握,那么这种方式显然有悖于老师助教们的初衷。因此在自己写代码的时候会首先分析哪些变量是共享变量,怎样的操作可能引发线程安全问题。投机取巧可能一时有效,但是最终吃亏的很可能是自己(想必很多第二次作业使用通用公式法的同学在写第三次作业的时候重构得快哭了吧)。完成作业固然重要,但是能够用更好的方式来实现,对我们的益处应该是更大的吧。

posted on 2019-04-21 14:10  Tinco  阅读(137)  评论(0编辑  收藏  举报