面向对象第二单元总结

OO第二单元作业总结

一、第一次作业

1.1作业要求

设计一台支持FCFS的电梯。

1.2设计思路

原本的设计是,一共两个线程,一个线程读取请求,一个电梯线程执行请求,这就是一个简单的生产者消费者的问题,只要把保证拿和取对象操作的原子性就可以了。但是和同学交流之后,为了以后的扩展性更好,添加了调度器线程(尽管后面的电梯也并没有用到),这样就变成了,生产者,消费者,二手消费者的感觉,或者说是二次生产者消费者的问题,调度器充当了前一个的生产者的消费者,充当了消费者的生产者。

1.3UML类图

在设计的过程中,想尽量的使每个类的功能分明,让电梯只做电梯的事情(运行),让调度只做调度,让读取请求的只读取请求。

1.3.1程序的运行逻辑

电梯获得请求之后具体的运行就是:判断请求是否为空,不为空就判断请求的fromfloor是否是现在的楼层,即是否现在需要让乘客in,如果不,则运行到fromfloor,让乘客in,再到tofloor,让乘客out,再获取下一个请求。如果这个是后请求为空,并且调度器和读取请求线程还没有结束,则会一直循环,知道请求不为空,或者另外两个线程死掉,那电梯线程也结束。

public void run() {
        while (schedule.isAlive() || rq.effective()) {
            PersonRequest q = rq.nextRquest();
            int sleepTime;
            if (q == null) {
                continue;
            }
            try {
                if (q.getFromFloor() != this.floor) {
                    sleepTime =
                            Math.abs(this.floor - q.getFromFloor()) * upSleep;
                    this.floor = q.getFromFloor();
                    sleep(sleepTime);
                }
                TimableOutput.println("OPEN-" + this.floor);
                sleep(openSleep);
                TimableOutput.println(
                        "IN-" + q.getPersonId() + "-" + this.floor);
                sleep(openSleep);
                TimableOutput.println("CLOSE-" + this.floor);
                if (q.getToFloor() != this.floor) {
                    sleepTime = Math.abs(this.floor - q.getToFloor()) * upSleep;
                    this.floor = q.getToFloor();
                    sleep(sleepTime);
                }
                TimableOutput.println("OPEN-" + this.floor);
                sleep(openSleep);
                TimableOutput.println(
                        "OUT-" + q.getPersonId() + "-" + this.floor);
                sleep(openSleep);
                TimableOutput.println("CLOSE-" + this.floor);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
电梯run部分代码

1.3.2uml类图

这里有五个类,Request,ReadRequest,Scheduler,Elevator,ElevatorMain,其中,ReadRequest,Scheduler,Elevator三个是继承于线程类。各自的功能就是,Request实际上是一个请求队列,里面增加了一些同步了的队列操作,ReadRequest读取请求,Scheduler将第一个队列的请求加入第二个队列,Elevator是执行电梯运行,ElevatorMain是执行创建并启动三个线程。

1.3.3uml协作图

注:Request1和Request2是类Request的两个实例。

1.4程序复杂度分析

第一次程序因为要求比较简单,算是一个入门,再加上每个类的功能比较清楚,所以复杂度应该是比较低的。从下面方法的复杂度里面可以看到,最高的是elevator.run(),这是因为电梯运行当中需要比较多的判断,判断请求是否为空,判断另外两个线程是否结束等,所以复杂度是比较高的。

1.5Bug和不足

Bug:当读取请求线程结束后,即使电梯还有请求没有完成,但是还是结束了自己。

原因:电梯结束的判断不是很正确,一开始我似乎是用的判断请求线程是否结束,以及请求队列中是否还有没执行的请求,来判断是否应该结束电梯线程,但是这样可能导致的问题是,请求线程结束了,调度器还没有从第一个队列把请求取到第二个队列,这个时候电梯线程判断了,结果就结束了自己,然后调度器才把请求挪到第二个队列。说到这个bug就不得不说判断电梯结束的条件,我最后设置的是调度器线程结束,并且第二个队列为空,而调度器结束的条件是读取请求线程结束,并且队列一为空。这样设置是比较合理的。

不足:为了判断调度器线程,读取请求线程是否结束,我采取了将调度器/读取请求线程作为成员变量传入电梯线程的方法,这样做似乎是不太合理的方法,正确的做法应该是,在线程结束后,产生一个信号,由信号去触发是否应该结束。这一点在后面有改进。

二、第二次作业

2.1作业要求

设计一台支持ALS的电梯。

2.2设计思路

这一次我把调度器线程删除了,因为在测试过程中,调度器线程只凭空增加了我可能出错的风险,而并没有什么实质性的好处,其余的基本还是保留了。所以这次的基本策略就是一个生产者——消费者的策略,不过区别于是上一次的循环等待的方法,这次用了notify()和wait()的方法,来取代原来的循环等待的方法,这样做似乎是为了减少对cpu的无用占用。

2.3UML类图

在设计中还是尽量的想让每个类的功能更加分明,但是似乎并不是那么容易能做到的,这里的设计没有设计特别好,导致有一些应该在电梯中的函数,写在了RequestQueue这个类当中,导致这个类并不只是一个Request Queue,而更多的包含了一些其它的功能函数,例如判断请求的运行方向,这并不太应该加在这个类当中。

2.3.1程序的运行逻辑

程序的运行逻辑总体上就是简单的生产者——消费者的运行逻辑,生产者获得请求加入队列,消费者从队列中取出请求,然后完成请求。这里值得说的是电梯的具体运行逻辑,因为这次执行的是可捎带的逻辑。

public void run() {
        boolean firstArrive = true;
        boolean elevatorIsOver = false;
        while (!rq.readIsOver() || rq.effective() || rqExecute.size() != 0) {
            if (rqExecute.isEmpty()) {
                PersonRequest temp = rq.lookRequest(0);
                setElevator(temp);
            }
            search();
            if (firstArrive) {
                firstArrive = false;
            } else {
                move();
            }
            if (haveInOut()) {
                open();
                passOut();
                if (rqExecute.isEmpty()
                    && !rq.effective() && rq.readIsOver()) {
                    elevatorIsOver = true;
                } else {
                    if (rqExecute.isEmpty()) {
                        PersonRequest temp = rq.lookRequest(0);
                        setElevator(temp);
                    }
                    search();
                }
                passIn();
                close();
                if (elevatorIsOver) {
                    break;
                }
            }
            if (elevatorDirect > 0) {
                up();
            } else {
                down();
            }
        }
    }
电梯运行逻辑代码

虽然看起来还是有一个大的while,但是实际上在查看是否有能取过来的请求的时候会等待,while实际上控制的是上下楼层,也就是while循环执行一次,只是上了或者下了一层楼。这里的逻辑和第一次的不一样在于,第一次作业的电梯是一次循环执行完成一个请求,而这里是一次上下一层楼。

至于捎带的逻辑,我是判断是否和电梯是同一个方向的,如果和电梯的运行方向相同,就会加入到执行队列当中。虽然这里的请求是给定了人的fromfloor和tofloor的,但是如果根据这个来判断和优化电梯的运行和捎带策略,会导致比较麻烦,这里有点偷懒,就只选择了判断是否和电梯的运行方向相同。

2.3.2uml类图

函数大致的功能和第一次差不多,只是删除了调度器线程,因为在这一次作业中,我实在觉得这个调度器线程没有什么太大的作用,所以就删除了。

2.3.3uml协作图

这里删掉了调度器线程,所以看起来简单了不少,后面的setOverTrue()就是设置读取进程完成的标志,然后readIsOver()就是判断结束标志是否有,然后effective()是判断请求队列是否还有效。

2.4程序复杂度分析

第二次作业,从上面我的电梯运行逻辑图也可以看出来,电梯运行的逻辑其实是比较复杂的,因为需要判断是否有人上下电梯,是否有人可以加入到可以捎带的请求当中来。

2.5Bug和不足

Bug:在测试的时候,发现电梯会超出运行一层楼,例如:电梯在一楼,有两个请求,一个是1-14,一个是4-2,这时候电梯会先将第一个人送到14层,这个时候电梯应该直接向下来接4-2的这个人,但是电梯却先上到了15楼,然后再下来接4-2的这个人。

原因:是因为电梯在下了1-14这个人之后,电梯内空了,但电梯的方向并没有改变,还是为向上运行。所以电梯又上了一层楼,到了下一层楼开始检测了,才将4-2的请求加入对列,然后下去接人。这是电梯运行逻辑不严谨造成的错误。改正的方法就是在电梯下人之后,判断是否应该更改电梯的运行状态。

不足:电梯内部的运行逻辑过于复杂,容易出错,应该抽象一些内容出去,以减少电梯运行逻辑出错的可能。比如将更改电梯方向,以及检查是否可以捎带其他人的内容抽象出去。

不足:电梯的捎带逻辑还可以进一步的更改,因为这部电梯不限容量,其实可以在运行过程中,碰见人就让他上来,然后通过电梯的最大或者最小运行楼层来确定电梯的运行。

不足:电梯确定运行方向的逻辑可以更优化,也就是取请求队列中离电梯最近的请求来确定电梯的运行方向,而不是取第一个,但是这样可能造成一些请求很久都得不到响应的问题,这很不符合现实,以及这样会增加出错的风险,所以并没有采用。

三、第三次作业

3.1作业要求

设计三台楼层容量等都不同的电梯。

3.2设计思路

这次的比较复杂,所以去掉了一些不必要的类,来减少可能造成的错误,只有调度和电梯两个类,电梯的运行和上一次的电梯大同小异,调度器是设计的主要部分。调度器实现的功能是,读取请求,按照一定的逻辑分给电梯。首先是对于可以直接到达的请求(即一部电梯便可以完成请求,而不需要多部电梯协作)选择分给当前请求数最少的,并且可以单独完成这个任务的电梯。其次是对于不可以直接到达的请求,从电梯楼层的分布不难看出,其实任何一个请求,至多两个电梯就可以完成,所以选择的策略是,将这个请求分给fromfloor在这个电梯可达的楼层,且请求数少的那一部电梯,由这一个电梯带到一个可以让另一个电梯直接送到的楼层,然后让他在这里下去,等待另一部电梯来接(此时对于这个电梯,这个请求便是一个可以直接到达的请求)。

3.3UML类图

在程序中还是想让类的功能比较分明,让调度器只实现分配请求的功能,让电梯只是获取请求,然后完成请求。

3.3.1程序的运行逻辑

首先是调度器的调度策略:

电梯的策略,几乎和上次的一样,只是多了判断容量的时候,如果电梯里的人满了,就不让上。

这里需要注意的是,到达一层楼的时候,必须要让人先下后上,不然可能导致错误。除此之外是对于不能够直接到达的请求,需要在这种请求下电梯的时候,产生一个新的请求添加到另外一台电梯当中。

 public void to(int desFloor) {
        elevatorDirector = setDirector(floor, desFloor);
        bufIn();
        if (haveIn()) {
            open();
            in();
            close();
        }
        if (elevatorDirector == 0) {
            return;
        }
        while (this.hasPassenger() || scheduler.shouldRun(this.getName())) {
            if (elevatorDirector == 1) {
                up();
            } else if (elevatorDirector == -1) {
                down();
            }
            scheduler.elevatorPrint("ARRIVE-" + floor + "-" +
                this.getName());
            if (accessible.containsKey(floor)) {
                bufIn();
                bufOut();
                if (haveOut() || haveIn()) {
                    open();
                    if (haveOut()) {
                        out();
                    }
                    if (haveIn()) {
                        in();
                    }
                    close();
                }
            }
        }
        elevatorDirector = 0;
    }
​ 电梯核心函数to的代码

3.3.2uml类图

这次就只建了电梯线程,而调度器线程糅在主线程当中,因为调度器的逻辑比较简单,所以就没有单独开一个线程(其实是因为如果单独作为一个线程,在主线程中在启动的话,会出现一些参数需要重新声明,赋值的过程比较麻烦)。然后电梯线程中就只是运行而已。

3.3.3uml协作图

电梯和调度器之间的交互主要就是从调度器当中获得乘客,以及有一个电梯结束的时候提醒其它的电梯醒来,准备结束。

3.4程序复杂度分析

可以看到电梯的函数to,复杂度爆了,这其实和第二次作业中的电梯run()函数爆的原因一样,需要判断的东西太多了,可以看到iv和v值几乎和上一次电梯的run()一模一样。其实to函数就是上次电梯run函数抽象出来的内容。

 

3.5Bug和不足

bug:在测试的时候,如果有那种只能由一部电梯携带,并且超过了容量的那种,会有一种反向携带的错误产生,类似于1-11个请求,电梯只能携带6个,本该携带完1-6之后,来携带7-11,但是却变成了只携带了7-11,而没有管1-6的请求。

原因:很明显可以看出来,是和判断容量相关的时候出现了错误,因为和容量的相关度太大了,所以只需要检查和容量相关的函数,最后检查到是在判断是否有人进入的时候,加了判断是否已经满员,但是这个时候人其实并没有上到电梯上,我却判断了满员。实际上判断是否有人进入电梯,只需要判断请求进入的队列是否有人就可以了。

不足:很明显,我这次的函数比较乱,而且类也没有那么分明,很多功能性的函数都写在了调度器当中,导致调度器不仅仅由调度的功能,还有一些其它的功能,例如输出,判断请求的运行方向等。感觉应该再根据功能更加详细的区分一下。

四、心得体会

线程安全:当涉及到多个线程对于同一个对象的操作时,一定要注意是否会导致线程安全问题,以及按要求的给对象添加锁。

线程安全:注意保证Read-Modify-Write和Check-Then-Act两种操作的原子性(对于可能出现线程安全的对象)。

线程安全:减少使用循环等待,而多用wait()和notifyAll(),这里也要注意如果只使用notify()还是可能造成死锁的情况。

设计原则:感觉就是尽量设计得简单就越好,想得太多不仅麻烦,而且容易导致错误。

posted on 2019-04-24 16:44  没心的先生懒  阅读(131)  评论(0编辑  收藏  举报