OO第二单元总结
一.设计策略
在这三次作业中,为了线程安全的考虑,我所有的容器都采用了CopyOnWriteArraylist。加锁时,我对各个list加锁,而不对函数加锁,锁的结构较为简单不会出现嵌套等现象,也就从根源上解决了死锁问题。
1.第五次作业
第五次首次通过多线程来解决问题,我使用了经典的生产者-消费者模型,除主线程外开启了两个线程,Input线程和Elevator线程。Input线程读取所有request并将request放入peoWaiting 的序列,Elevator线程根据SSTF算法从peoWaiting中读取request,并将request运送到目标地点。Input输入结束时会将InputState置为true,而Elevator线程读取InputState,如果为true且peoWaiting序列已空且电梯内不再有人,则结束线程。
2.第六次作业
第六次我也采用了生产者-消费者模型,由于要操控5部电梯所以我用Scheduler类来完成对request的分配工作,也加入了一个类叫做schedulerState。Input将读入的request写入list,当输入结束时将InputState置为true。scheduler给每部电梯的等待序列listA,listB,listC,listD,listE加入request。当scheduler的list为空且读取到输入已经结束时,则将schedulerState置为true,scheduler线程结束。Elevator线程读取schedulerState,如果为true且peoWaiting序列已空且电梯内不再有人,则结束线程。
为了线程安全的考虑,我所有的容器都采用了CopyOnWriteArraylist。加锁时,我对各个list加锁,而不对函数加锁,锁的结构较为简单不会出现嵌套等现象,也就从根源上解决了死锁问题。
3.第七次作业
我第七次作业的架构与第六次基本相同,只修改了scheduler分配的算法,增加了作业中要求的楼层限制以及人数限制等条件。为了满足此次作业乘客转乘的需求加入了Person类,Person类的属性有id,起始地点,目标地点等基本信息,还有是否正在转乘的标志。因为每个人必须在自己完成第一次转乘后才能第二次转乘,所以在第一次转乘时scheduler必须等待,等该人第一次转乘完毕后再进行第二次转乘。
二.第三次作业的可扩展性
我觉得我的三次作业相对比较符合solid原则。
单一职责原则:我的Main类只负责各个变量的声明和线程的启动,scheduler类只负责将input类输入的request调配给不同的电梯运行,input类只负责request的输入,elevator类只负责电梯的运行,inputDone和SchedulerDone类之负责input和schduler线程的状态,总之分工明确。
开闭原则:我借鉴了一些策略模式的思想,所以scheduler的调配策略几乎可以和其他类解耦,比如我完成第六次作业到第七次作业的迭代开发时几乎只需要修改scheduler的调配策略,其他类相当于对修改是关闭的。
里式替换原则:本次作业无继承,故不讨论。
接口隔离原则:本次作业无继承,故不讨论。
依赖反转原则:本次作业无继承,故不讨论。
三.程序结构
由于我第六次与第七次的程序结构基本相同,所以在下方只保留了第五次和第七次作业的有关分析。
1.第五次作业
UML类图如下图所示:
本次作业结构比较简单,除了主线程外只有Input和elevator两个线程。
UML协作图如下:
2.第七次作业
UML类图如下图所示:
UML协作图如下图所示:
结构复杂度如下图:
可以看出Elevator类和Scheduler类的复杂度明显较高,因为我几乎所有有关电梯调配的策略全部都放入了scheduler中导致scheduler类非常冗长而且复杂度也比较高。而其他几个类由于几乎只有属性,get方法和put方法导致复杂度很低,成为了“傻瓜类”。总之,要通过将一个类拆为两个类和将一个类的方法转移到另一个类中来避免“上帝类”scheulder和elevator类,以及“傻瓜类”Person的出现。
我想到的降低scheduler复杂度的方法是分级调配,将一个scheduler拆成两个类,把对电梯中的人的的调配单独变成另一个scheduler类。
我想到的降低elevator类的方法是将人员上下电梯的getIn()和getOff()方法从电梯类中拿出放入Person类中,从而降低elevator类的复杂度,提高person类的复杂度。
四.自己的Bug
由于第五次作业比较简单,我的第六次与第七次作业的结构基本相同,只是换了一个scheduler把人分配给电梯的策略,所以我第五次和第七次作业基本没有出什么bug,而第六次作业课下与提交后发现的bug比较严重。
正如前文中所述,由于我选择给生产者-消费者的容器上锁,所以锁的结构比较简单,从根源上避免了死锁的问题,我主要出现的bug是while轮询导致的CTLE和线程无法正常终止的RTLE。
1.第五次作业
强测互测中均无bug。
2.第六次作业
- 我出现的第一个bug是RTLE的问题。
图片中修改的目的是消除我强侧中出现的RTLE现象。InputState是我设置的input的状态,当Input线程读到NULL后,会将InputState类里的inputDone属性设置为true。当读到NULL时,input线程会唤醒正在wait的scheduler线程,如果scheduler线程判断此时的inputDone为false说明输入还未结束,不退出线程。如果inputDone为true说明输入已结束,便退出线程。scheduler对应此部分的核心代码如下图所示:
如果Input类先唤醒scheduler线程然后再修改inputDone的值,如原先的代码一样,就会导致此时inputDone尚未修改而scheduler已经被唤醒却监测到inputDone为false,就会继续进入循环。修改后首先设置inputDone属性为true再唤醒sheduler线程,便不会出现这样的问题。
2.我出现的第二个bug是CTLE的问题:
如下图所示,go()方法表示电梯需要从start楼层走到end楼层。在修改之前,我只有if(start<end)和else if (start>end)两个分支,并没有下面的else分支。我才用的算法是SSTF算法,电梯永远只会前往正在等待的人的起始楼层与电梯内的人的目标楼层中与现在楼层绝对值最小的楼层。而如果电梯输入的时候比较巧,比如电梯刚刚从第10层出发,来了一个起始楼层为第10层的人,按照SSTF算法电梯下一次停靠的楼层依然为第10层。而由于go()方法初始时并没有else分支,导致go()方法直接返回到while循环中。由于SSTF算法的死板性,电梯一直想去第10层接一下这个人而由于没有else分支无法接这个人上电梯,从而导致elevator类一直处于while循环而不能进入wait(),从而导致轮询的出现和CTLE的发生。
3.第七次作业
我在第七次作业中出现的bug是只能添加X1,X2,X3三种电梯,按照作业要求电梯的编号可以是任意整数
五.别人的Bug
- 我在互测中发现了两种bug:
第一种,在第七次作业中只能添加X1,X2,X3三种电梯,按照作业要求电梯的编号可以是任意整数,而有的同学(包括我自己)却只能添加这三种电梯,而且巧妙地躲过了强测,因为强测只加了X1,X2,X3这三种电梯。
第二种,在互测中有的同学遭遇边界条件时会发生死锁等线程无法正常终止的现象。如所有请求在同一时刻输入时就会RTLE。
2.纯随机数据往往不够强,需要构造比如同时输入的边界数据。
六.心得体会
1.线程安全
在这三次作业中我采用的所有容器都是线程安全容器CopyOnWriteArrayList,为了从源头上避免死锁的出现,我并没有选择用synchronised给方法加锁,而选择给生产者和消费者使用的容器list加锁,这样就不会出现锁嵌套等的现象,也就从根源上解决了死锁的出现。如下两张图分别为scheduler和elevator类(即生产者和消费者线程中对list加锁的部分)。
2.设计原则
本人主体使用的设计模式是生产者-消费者模型,并借鉴了一些策略模式的思想。在完成第六次作业到第七次作业的迭代时,我基本只需要修改sheduler类中关于调配request给电梯的算法。而其他类中的代码基本不需要改变,这比较符合策略模式的思想。然而这也导致了一个问题是我的scheduler基本负责了所有的调配工作导致比较冗长,基本有400行,属于“上帝类”,而且与其他类的耦合度有点太高,总共有21个参数。
3.评测机的重要性
评测机真的太重要了!我第六次作业出现了比较严重的bug,主要原因之一是当时我的评测机还没写好。
第7次作业我的成绩较好,获得了99.4分,原因之一是我写好了评测机,debug 的速度成指数上升。
4.一个疑问
在第三次作业互测时,我强侧得分99.4应该在A屋,但是我发现屋内基本没人使用线程安全容器CopyOnWriteArrayList而是基本都自己手动实现了生产者-消费者模型的容器tray,这也确实让我感到比较奇怪。如果有谁读到了我的文章且有自己的理由,请在下方评论区一起讨论。