面向对象第二单元个人总结

上一次博客总结是依据几次作业顺序分析的,感觉并不能很好的构成体系。这次打算按照几个方面的顺序进行总结。

(1)设计策略:单次Scan一台电梯->三台电梯。

第一次作业:

  实际上写得就是可捎带的,所以思路上可以说是第二次作业的。但是第二次作业比起第一次有很多成熟和改进,也获得了比较良好的性能分。

先说第一次的初步设计。

第一次作业要求:

电梯系统时间:程序开始运行的时间

电梯楼层:1层到15层,共15层

电梯初始位置:1层

电梯数量:1部

电梯上升或下降一层的时间:0.5s

电梯开关门的时间:0.25s。开门0.25s,关门0.25s,共计0.5s,到达楼层后方可立刻开门,关门完毕后方可立刻出发。

  一个非常自然的想法,我建立了一个WaitingPeople 线程类,负责把人从request中提取并且加到队列里。再建一个Elevator线程类,就是模拟电梯的运行。一个Floor类,负责电梯在每一层人的行为。

  而这些类都有一个公用对象,通过这个公用对象交流和协作。这就是WaitingList类,其中包括两个重要的内部变量Hashmap ele 和wai,一个是电梯中人的队列,一个是电梯外等待各个人的队列。在WL类中定义了所有可以使用更改ele和wai的方法,就是说ele和wai是不暴露跟外部的接口的。

  在MainClass建立一个WaitingList 类对象waitlist,并在构造每个WaitingPeople(来一个人), Elevator(只有一个),Floor(每停一层)时就传入同一个waitlist。这样waitlist只产生一个,所有类使用它,通过它互相通知和联系,如下图所示。

  

   这几个类中,这次只有addListW需要Synchronized因为只有人员是可能多个线程争抢这个函数的,其它都由电梯行为确定,一个时间只有一个线程运行,并调用相关函数。

   整个方法设计就是基于两个队列wai、ele。wai hashmap的key是等待的楼层,如“1”,“3”。自己设了内部类Plan,包括人员id和一个楼层字符串,在wai中是目的楼层,ele中无需信息但默认为来的楼层。Hashmap中元素都是Plan数组,因为等待人员较少,可以用静态数组。每次加人更新数组,每次到达一层,就将wai中key为该层元素直接删除(enterE)。ele是以目的楼层为key的,初始为空,每次到达一层,就将key为该层的元素删除(exitE),然后把wai中删除的元素,依原来Plan里面的floor做key加入ele。这个过程微观上实际上是遍历wai中该层元素Plan[]中Plan,依次加入ele,再最后删除wai中一层。

   但要实现捎带,这些都不是最重要的,最重要的控制器,相当于被我放到了一个nextFloor方法里面(或说WaitingList 作为公共交流区自动成为控制器,并通过nextFloor实现)。其实就是一个方向优先的单次scan算法,每次找寻同方向最近的需要开门楼层(有人等或有人下),如果没有再找反方向,且每层更新一次。虽然不是最优,不能掉头,但是尽量模仿了现实电梯的行为(使乘客能预估电梯的运行,而不是看它快到了莫名奇妙调个头。)这个方法沿用到第二次性能是不错的,拿了93。但它也为第三次作业的bug埋下了足够明显的祸根。

第二次作业:

  在第一次作业下进行了一些调整,并在这里谈谈如何判断电梯休眠、结束程序。首先还是作业要求。

电梯系统时间:程序开始运行的时间
电梯楼层:-3层到-1层,1层到16层,共19层
电梯初始位置:1层
电梯数量:1部
电梯上升或下降一层的时间:0.4s
电梯开关门的时间:开门0.2s,关门0.2s,共计0.4s,到达楼层后方可立刻开门,关门完毕后方可立刻出发。

   电梯休眠和结束主要依托以下循环结构,具有一定的圈复杂度。

  nextFloor在当前没有任务时会返回一个-10,表示没有需要开门的楼层,这时候,如果getEnd() == 0,说明输入结束,不会再有新的乘客了,此时跳出while(true)循环节,线程结束。那如果还可能有新的乘客,他将会在未来某个不可预知的点到来。在第五周我采取的是休眠0.5秒,会影响性能,但是由于总时长的限制,不会产生轮询那样巨大的cpu耗费。而第六周考虑到性能及拓展,在WaitingList类中加入waitForAwake,将其与addListW方法用同一个对象锁wai锁住,这样addListW后可以notify all,跳出wait()循环条件是wai是否为空。为什么while(true)下有个sleep呢?这是因为如果是同时插入的人,希望在全部入队后再进行nextFloor判断,避免电梯已经启程前往某随机的同时入队乘客目的地,而不是一起的人中最优解。

public void run() {
        int next;
        while (true) {
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            next = waitlist.nextFloor(floor, direction);
            if (next == -10 && MainClass.getEnd() == 0) {
                break;
            } else {
                    if (next == -10) {
                    WaitingList.waitForAwake(); //wait notify
                    } else {
                        //正常查找
                    }
           }
     }
}    
 0层的特殊,只需要在nextFloor和arrive时跳过continue即可。

第三次作业:

  完成多部多线程智能(SS)调度电梯,要求如下:

 
电梯系统时间:程序开始运行的时间
  电梯初始位置:1层(三部电梯均是。电梯系统运行之中停止等待的楼层可自定义)
  电梯数量:3部,分别编号为A,B,C
  电梯可运行楼层:-3-1,1-20
  电梯可停靠楼层:
  A:  -3, -2, -1, 1, 15-20
  B:  -2, -1, 1, 2, 4-15
  C:  1, 3, 5, 7, 9, 11, 13, 15
  电梯上升或下降一层的时间:
  A: 0.4s
  B: 0.5s
  C: 0.6s
  电梯开关门的时间:开门0.2s,关门0.2s,共计0.4s,到达楼层后方可立刻开门,关门完毕后方可立刻出发。三个电梯均为此时间。
  电梯最大载客量(轿厢容量)
  A:6名乘客
  B:8名乘客
  C:7名乘客
  电梯内部初始乘客数目:0

  这次与前两次,我的设计上就要有比较大的改变。首先是对于WaitingList类,它不再管ele了,ele成为每个Elevator类自带的hashmap,所有对于elev的改变都在Elevator类完成。这样在思维上比较容易理解,载客量和可停靠层数都是电梯类的性质还有人数pnum初始化为0,在构造时赋予每个新电梯,同时每个电梯都有自己的static队列。这里一定要注意ele不能是static类否则只会初始化一次,等于三个电梯公用了一个ele!

  这里的控制器重要的是如何调控三个电梯的运行和停止。首先是对于公用WaitingList中wai的访问的协调,把几个方法nextFloor,enterE都Synchronized限制访问。在WL类中,构造时就传入三个电梯A,B,C,同时需要Elevator类提供的外接口getEMap。这样调用nextFloor,enterE时就先getEle(size)通过传来的size或type选择对应得电梯,返回Elevator。nextFloor中还需要Elevator.getEMap获得ele,以判断其中有无人员来规划电梯运行方向。

  本质上第三次没有特别大得改变,电梯运行控制器仍然由WL类控制,exitE和addListE是由电梯到达直接触发,和由enterE(WL类中)控制,本身不控制电梯行为。看似好像直接冲突解决了,不会出现一起访问wai的情况,但是对于nextFloor却有隐藏冲突。这将在Debug中纤细讲述。

(2)基于度量来分析自己的程序结构:圈复杂度控制

第一次作业:

 

第二次作业:

和第一次作业框架相似,所以在这里展示UML的协作图(sequence diagram)。

第三次作业:

 

 

个人优缺点分析:

优点:

  思路比较接近于现实状态,每个类所负责的任务比较清晰。

从行为上来讲,只有人和电梯是线程,三家电梯和人员到来是相互独立的, 通过WL类来进行交流。

这一点在最后一次作业的UML图上表现的比较清晰。

缺点和所存在的问题:

  1,圈复杂度把控仍然不够。

  像elevator.run函数这种圈复杂度严重超标。现在我们来看看代码。代码中划掉的部分是完全可以封装成小函数,绿色的部分我认为可以直接调出来单独成为一个方法,然后其中包括划掉的小函数。听大佬说还有stream方法,这次还不太了解,希望好好学习上课后能够知道怎么使用。

public void run() {
        int next;
        while (true) {
            napWait();
            if (isFull()) { //full
                next = nextEle(floor, dire); //找下车点 一定有
            } else {
                next = waitlist.nextFloor(floor, dire, size);
            }
            if (next == -10 && waitlist.Allemp()
                    && MainClass.getEnd() == 0) {
                waitlist.ChangeEnd();
                break;
            } else {
                if (next == -10) {
                    WaitingList.waitForAwake(); //wait notify
                } else {
                    while (true) {
                        int i;
            if (next > floor) {
                            dire = 1;
                            for (i = floor + 1; i <= next && next != -10; i++) {
                                next = arriveFloor(i, next);
                            }
                            if (next <= floor && next != -10) {
                                floor = i - 1;
                                continue;
                            }
                        } else if (next < floor) {
                            dire = 2;
                            for (i = floor - 1; i >= next && next != -10; i--) {
                                next = arriveFloor(i, next);
                            }
                            if (next >= floor && next != -10) {
                                floor = i + 1;
                                continue;
                            }
                        }
                        break;
                    }
                    if (next != -10) {
                        TimableOutput.println("OPEN-" + next + "-" + type);
                        runFloor(next);
                        TimableOutput.println("CLOSE-" + next + "-" + type);
                        floor = next;
                    }
                }
            }
        }
    }

  2,控制逻辑还是没有完全分离。

  这次的WL类不仅仅是个单纯的控制器,它还同时负责对于队列的操作,这就使得它的函数方法多而复杂,同时不那么容易拓展。

毕竟WL类的本意是建立一个等待队列,而不是控制系统。更好的重构方法还是要把WL与控制器分离。

除此之外,我发现为了封装逻辑降低圈复杂度,在每个类下都建立了很多方法小函数,它们即用即建,彼此之间的逻辑包含关系并不清晰,

命名也较为混乱。如果单建一个公用方法类,并且分门别类组织会比较好。我现在还没有使用接口,也许用一些抽象方法可以使程序更易懂。

 (3)分析自己程序的bug

  这次第一次第二次作业都没有在公测和互测中被测出bug,因为从第一次开始就是为了第二次的框架而设计。但是第三次作业便出现了问题,且bug都在于

nextFloor函数之中。

  这个函数在一二次作业的作用就是给唯一的一个电梯寻找下一个开门层,且上一层更新一次,尽量保证捎带。两次开门之间的更新是没有考虑出现没有目标(返回-10)的情况的。因为只有一架电梯,只要在开关门后第一次搜索找到目标,之后更新可能会更新到更近的楼层捎带,但不会出现没有目标的情况,因为原来找到的目标不会自动消失。

  然而在三架电梯中,前面找好的目标可能会被其它电梯抢走,自动消失。这时候就返回-10。前面两次用了很长时间思考规划,这次还是太仓促了,三架电梯仍然套用原来的nextFloor方法。这样当然中测就会有问题,于是我就考虑了!=-10才开门,同时考虑了更新的楼层由于原先目标的消失,可能换方向或不在原目标和起始楼层之间。这样虽然过了中测,还是遗留了几个bug给强测。

  首先是对于变换了方向或范围的目标楼层,我直接continue到上一级循环节以重新判断改变方向。但是上一级循环节是默认没有-10的,而-10也符合超出range的判断条件,这就出现把-10当正常目标层的情况。解决方法是加上!=-10的判断条件,使-10直接进入最大的可处理-10结果的循环。

  其次,在直接continue前我没有更新起始楼层,因为原来只有开关门才更新起始楼层,中间的更新用迭代量i就解决了。这就致使楼层已经变了,还以为在上次关门的地点,并以此重新计算nextFloor。解决方法是在arrive时就更新楼层。

总结bug与结构关系:

  首先是从一个电梯往三个电梯迁移,不能仅改几个数,而要重新考虑原来的控制器方法适用与否。重新尽心细致的思考,并画出线程图,考虑在各个环节可能发生的情况及应对策略。起始这几个bug真的不难想到,只要仔细的思考过。

  其次,这和我的控制器没有与线程抽离开有关吗?有关。这里的控制流还是集中在Elevator和WL类,其实是不对的。更好的重构方案是抽离一个控制器,并将Elevator的控制流尽量简化,还是要画图!!!线程一定要画图,把每个判断点调用控制器函数解决。这样就不会出现,Elevator的run函数有许多的控制流,容易遗漏出错。

 (4)分析自己发现别人程序bug

  自己这次虽然没有找到别人的bug,但是还是发现了很多和第一单元作业不同的分析bug点。以第三次作业来说吧,我发现很多同学和我的思路是类似的。第一单元的bug主要集中在WrongFormat,难点是利用符号树,怎么样排除掉所有的异常格式;还有输出,如何保证又简洁又正确,比如我就有丢了+号导致出错的情况。

还有就是递归问题,如何递归到没有错误。这个程序架构是层层处理,要找就是找哪一层可能出了问题,丢了情况。大概如下图所示:

  而电梯则不一样,最容易出错的点有几种:线程混乱(访问和插入操作没有分离开,变量没有分离操作),无法送达(可能是队列处理问题),电梯行为异常(像我那种没有及时更新电梯信息-楼层-人员-类型-状态),无法正常休眠结束(提前结束,永不结束)。

  

   保证线程安全,就是保证访问同步;线程思路就是把一个线程一遍遍思考,保证每一步操作是正确的,考虑所有情况的,再把一步步操作按部就班执行。最后就有可以运转的电梯系统了。

(5)心得体会——线程安全与设计原则

  这次先从线程安全说起,最经典的两点,生成类的权限以及预防死锁。

  首先对于方法进行Synchronized就不必多说了,保证线程同步访问某些临界区函数。生成类的权限我认为也很重要,这里的waitlist和elevator数量都是确定的,并且自始至终存在的都是开始生成的那些。这就需要传参,让Elevator类中都接着同一个WL类接口,而WL类接着三个Elevator接口,保证可以互相交流。它们彼此都是对方的观察者。WaitingPeople线程也是传同一个WL类,只改变它,而它的改变引起Elevator联动。为了保证线程安全,WL类始终是不暴露自己的内容的,只给外界提供接口,只有其中的函数能改变自己,这就保证了所有操作都围绕着一个waitlist进行。

  预防死锁主要是在wait()和notify那里,等待中要保证能被唤醒。我设的是addListW()唤醒,睡眠前提条件是还有输入,只有在保证会被唤醒时才进入睡眠。而到三个电梯时,可能会由于目标被抢走而自动休眠,此时不保证会再有新人加入被唤醒。就加上结束前唤醒机制,如果判断不再有新人,且所有队列为空,便唤醒其它所有线程进入最后一次循环,该线程结束,其它被唤醒线程进入循环后在同一条件下结束。

  然后是线程设计,这次我觉得比起第一次作业还是有进步的,整体框架已经有所考虑了。改进方向就是像之前所说的,分离控制器和执行线程;减少控制流,多封装数据。

  整个线程安全的学习还是很有意思的,学知识且实践。这次前两次作业不错,第三次大意了,以后对于框架迁移要多思考,自己的测试也要做得更加完备。

猜你喜欢

转载自www.cnblogs.com/jura/p/OO_unit2_juracera.html