Loading

OO第二次总结

OO第二次总结

一、架构设计体验

本次作业的主要目的,是实现一个多楼座(A,B,C,D,E)、跨楼座运行,支持换乘,调度等功能的多线程电梯。

对于该问题,我主张采用经典的生产者-消费者模型。从Person类出发,构建了相应的共享资源,TableDispatcher等等。

线程及同步控制块与锁

在这一次电梯的作业架构钟,我创建了三种类型的线程:输入处理线程(InputHandler)、乘客请求分发线程(Dispatcher)、以及电梯线程(Elevator)。

共享资源的同步控制

public class PersonQueue {
    private static final PersonQueue WAIT_QUEUE = new PersonQueue();
    private static final PersonQueue TRANS_QUEUE = new PersonQueue();
    private static final HashMap<String, PersonQueue> PROCESSING_QUEUE = new HashMap<>();
    private ArrayList<Person> persons;
    private boolean isEnd;

    public PersonQueue() {
        this.persons = new ArrayList<>();
        this.isEnd = false;
    }

    public static PersonQueue getWaitQueue() {
        return WAIT_QUEUE;
    }

    public static PersonQueue getTransQueue() {
        return TRANS_QUEUE;
    }

    public static HashMap<String, PersonQueue> getProcessingQueue() {
        return PROCESSING_QUEUE;
    }

    public synchronized Person getOne() {
        //TODO: this is not equal to exp version
        while (persons.isEmpty() && !isEnd) {
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        if (persons.isEmpty()) {
            return null;
        }
        Person ret = persons.get(0);
        persons.remove(0);
        notifyAll();
        return ret;
    }

    public synchronized void put(Person p) {
        this.persons.add(p);
        notifyAll();
    }

    public synchronized ArrayList<Person> getPersons() {
        return persons;
    }

    public synchronized void putAll(ArrayList<Person> ps) {
        this.persons.addAll(ps);
        notifyAll();
    }

    public synchronized void setEnd(boolean isEnd) {
        this.isEnd = isEnd;
        notifyAll();
    }

    public synchronized boolean isEnd() {
        return isEnd;
    }

    public synchronized boolean isEmpty() {
        return persons.isEmpty();
    }
}

本次对共享资源的控制实现机制为:在有投喂请求且获得共享资源锁的情况下,直接投递相应的共享资源。并唤醒等待中的所有线程去获得新请求。而对于消费者线程,既电梯,在没有新的请求到来且输入尚未结束时,会在自己所拥有的共享资源上等待。并继续执行之前的行为。

调度器分析

这一次作业的要求中,我们需要完成同一座楼层多部电梯的协同,以及不同楼座横向电梯之间的换成等行为。为此,我的调度器分为以下几个行为逻辑。

  • stay:在当前乘客列表为空,且无新请求的情况下。
  • stay:每到一层楼调用controller的方法,判断下一次前进放向,上下乘客。
    • 首先根据当前到达楼层,获得乘客列表中所有需要离开电梯的乘客。
    • 根据新乘客列表,更新电梯当前状态。
      • 若还有乘客,则依据乘客的需求前进。
      • 若无乘客,则判断上一次状态,若上一次的方向上还有请求则同方向,否则,转向。
    • 根据更新后电梯的状态,筛选上电梯乘客。

在这个过程中,对于多部电梯的情况,我采用了多部同类型电梯共享一个资源对象,并通过在电梯控制时,synchronize标记共享资源,实现互斥访问。因此对于每一个电梯的调度行为,都是原子性的,不会由此发生线程安全问题。

同时,针对多部电梯,采用自由竞争方案。这样做的好处,我认为是在局部上为最优的贪心策略。既我不考虑各种***钻的边界情况,让电梯的性能决定载客行为。

而之后为了满足乘客出发楼层、楼座和目的楼层、楼座都不相同的换乘情况。我针对三种不同的乘客路线进行对乘客类的相应封装。

对乘客类:

	public int getFromFloor(String building) {
        if (fromBuilding.equals(toBuilding)) {
            return fromFloor;
        } else if (building.equals(fromBuilding)) {
            return fromFloor;
        } else {
            return transFloor;
        }
    }
    
    public int getAimFloor(String building) {
        if (fromBuilding.equals(toBuilding)) {
            return toFloor;
        } else if (building.equals(fromBuilding)) {
            return transFloor;
        } else {
            return toFloor;
        }
    }

对外部电梯调度器,通过指定当前乘客所属楼座或楼层,得到相应不同楼座下乘客的出发点与目的地,实现对前两次作业中电梯的复用。

而对于乘客中转的操作,我的实现策略是,新增一个TransDispatcher类。对于任何一个出电梯乘客而言,会被电梯放入一个transQueue中,并由TransDispatcher判断其是否需要放回电梯的共享资源中完成之后的路程。

扩展性分析

UML图

协作图

架构分析

对于可拓展性问题,首先针对功能上的拓展;例如电梯实现更多变的功能,比如楼层可达性等等,由于本实现架构为电梯控制器和电梯类分离,因此可以仅更改电梯控制器的逻辑,以及相应输入解析逻辑便能完成;同时针对其余,如新乘客需求等等任务,都可以针对本架构实现原有类的包装,使其暴露的接口同这次作业相同完成相应的功能。

为了实现本次新增的换乘功能,在原有策略上,新增了换乘调度器,由其负责乘客出电梯后的分配工作,实现了与原有架构的高内聚,低耦合。每个模块仅完成其自己的功能。

二、问题、bug、共性问题分析探讨

针对不同次作业出现的bug、及共性问题在此处探讨。

作业五

作业五中,我及周围同学所面对的最大的问题是输出时间戳不递增,对于这个问题,大部分同学都是忽略了输出的线程安全性所导致的。对于java内置的大部分包,几乎都不是线程安全的类,除了少数的原子性类,多个线程对其进行操作都是不安全的。因此,在调用这些共享资源时,我们需要获得相应的锁,使得在每一次操作时有且仅有一个线程能对其进行读写。从而实现线程安全。

除此之外,我还犯了一个严重的错误,对于电梯容量问题的处理不当,我采用了List作为存储乘客的容器,并在赋值时错误地使用了向下类型转换,导致失败报错。且该问题在中测,以及我自己的测试中完全没能体现。这是由于对于基本测试的不全面性导致的。

作业六

针对作业六,我在互测中暴露出来的bug主要是CPU轮询。对于轮询这个问题,由于在询问共享资源状态的函数比如isEndisEmpty中,多余地对拥有共享资源监视器的线程notifyAll。这会导致,等待新乘客的线程被频繁唤醒,占用大量CPU时间。所以在之后的作业中我明晰了以下两条。

  • 在对目标进行写操作、或更新状态时,需要notifyAll(),来保证等待的线程能获得新资源。
  • 在对目标进行读操作时,不需要notifyAll(),因为读操作并不会给等待线程带来新资源,因此避免notifyAll()频繁唤醒。

作业七

针对作业七,我了解到的部分同学存在的共性bug与轮询有关,既同我之前所讲。同时有一部分同学存在,换成过程中,电梯因为接错人的问题,来回震荡。也有可能去往不该前往的楼层。而我自己在这一部分暴露出来的问题是,线程安全结束的问题。由于我的逻辑是,在输入结束以后才会判断是否需要停止所有线程,且该判断是基于乘客请求的数量。这会导致在没有新乘客输入,但有新电梯输入时,由于结束判断在等待新乘客无法被唤醒,导致无法判断是否应当结束的问题。

对于该问题,我的后续解决方案是,在结束时送入一个特别的乘客请求,标志输入结束来避免这个问题。

发现他人bug的策略

我所采用的策略为:构造边界数据+随机数据测试

构造边界数据:该方法分为两类,一类是行为边界数据,一类是性能边界数据

  • 行为边界数据是指:考虑电梯的运行逻辑等待,例如第七次作业中出现换乘,可以需要不同换乘模式的乘客进行测试。比如,需要先到达换乘楼层再换乘,或者已经到达换乘楼层,直接换乘等等。或者是考虑电梯结束策略,在不在送入新乘客后,新增电梯请求。
  • 性能边界数据是指:考虑电梯极限状态下的运载逻辑是否正确,比如在同一时间塞入大量乘客,测试代码对于极端情况的鲁棒性。

构造边界数据的方法非常有效,因为该方法是针对本次作业中极易出现bug的地方,进行针对性测试。这也是我们许多同学存在的共性问题。

随机数据测试:则是由Python脚本生成随机数据,由随机数据指导hack,本次作业中,该方法效率较低,因为平常逻辑的电梯,很难出现重大问题。对于未针对的数据,往往只能测试到平常的一些点。很难有效的直接命中。

三、度量分析

类名 OCavg OCmax WMC
base.Person 1.65 7.0 33.0
base.PersonQueue 1.18 3.0 13.0
controller.BEleController 5.33 13.0 32.0
controller.FEleController 3.88 11.0 31.0
dispatcher.InputHandler 4.0 9.0 24.0
dispatcher.PersonDispatcher 2.0 4.0 6.0
dispatcher.TransDispatcher 2.5 7.0 10.0
elevator.BuildingElevator 1.42 6.0 30.0
elevator.FloorElevator 1.47 5.0 31.0
MainClass 2.0 2.0 2.0
util.CircleUtil 3.0 5.0 9.0
util.StringUtil 1.0 1.0 1.0
Total 223.0
Average 2.10 5.69 17.15

由类复杂度可以看出,由于本次作业我未针对不同电梯类,以及相应的控制器进行抽象,层次化处理,相应的部分复杂度较高。特别是Controller 类涉及不同电梯的不同行为逻辑和模式,出现了较大的重叠部分,导致其调用复杂度偏高。

四、本单元学习的心得体会

本单元我们学习了多线程,同步控制等知识。这在OO课程中,我认为是提供给我了一个入门的接口。并让我了解到了更多,高阶的同步控制方法如ReEntryLock

同时也了解到了,多线程中经典的一些模型,例如生产者-消费者,单例模式等等。这对我之后对于多线程更深一步,其他方面的学习也会有所帮助。

posted @ 2022-05-02 09:21  carkham  阅读(16)  评论(0编辑  收藏  举报