面向对象第二单元总结

王思月-21373349 2024-04-16 21:38:28

面向对象第二单元总结

第一次作业分析

UML类图及协作图

img

img

代码架构分析

本次作业主要关注三个问题:

  • 如何运用生产者-消费者模型实现多线程
  • 如何通过上锁解决线程同步问题
  • 电梯采用何种调度策略

生产者-消费者模型

输入线程InputThread获取用户请求,为生产者;电梯线程Elevator处理用户请求,为消费者请求队列RequestQueue用于存放请求,本人为每个电梯实例化一个专属的请求队列,用于存放即将乘坐电梯的乘客。

请求 Person

本次作业中的请求内容包括乘客编号、起始楼层和目的楼层,主要内容如下:

public class Person {
    private final int id;
    private final int fromFloor;
    private final int toFloor;

    public Person(int id, int fromFloor, int toFloor) {
        this.id = id;
        this.fromFloor = fromFloor;
        this.toFloor = toFloor;
    }
}

每个请求在输入线程中,会根据指定的电梯存放到对应的请求队列中。

请求队列 RequestQueue

请求队列的设计参考上机作业,主要内容如下:

public class RequestQueue {
    private final ArrayList<Person> people;
    private boolean isEnd;

    public synchronized Person getOnePerson(int floor, int direction) {
        if ((!isEnd) && people.isEmpty()) {
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        if (people.isEmpty()) {
            return null;
        } else {
            for (Person person : people) {
                if (person.getFromFloor() == floor && (person.getDirection() * direction > 0)) {
                    //同层同方向
                    people.remove(person);
                    return person;
                }
            }
            return null;
            //已经get了本层所有能上电梯的人
        }
    }
}

当消费者从请求队列中获取请求时,只获取同层且目标方向与电梯运行方向相同的请求

锁的设置

请求队列会同时被输入线程和电梯线程访问,因此需要加锁避免线程安全问题。本人对RequestQueue类中,所有涉及成员变量读写的方法都加了synchronize锁:

    public synchronized void addPerson(Person person);
    public synchronized Person getOnePerson(int floor, int direction);
    public synchronized void waitPerson();
    public synchronized void setEnd(boolean isEnd);
    public synchronized boolean isEnd();
    public synchronized boolean isEmpty();
    public synchronized int getSize();
    public synchronized ArrayList<Person> getPeople();

调度策略

本单元的电梯运行均采用Look策略,具体内容如下:

每台电梯初始的运行方向均规定为向上。

当电梯运行到某一层时,首先判断是否需要开门

  • 如果有乘客的目的楼层为当前层,则开门让乘客出去。
  • 如果当前楼层有乘客的目的方向与电梯运行方向一致,且电梯没有满员,则开门让乘客进入。

如果不需要开门,则判断电梯里是否有人

  • 如果电梯里有人,则电梯按原运行方向继续运行。
  • 如果电梯里没有人,则根据请求队列判断。

请求队列为空时,如果输入结束,说明之后不会再出现乘客,电梯运行结束;如果输入还未结束,则等待之后可能出现的乘客。请求队列非空时,判断电梯运行方向上是否有发出请求的乘客,如果有则电梯按原运行方向继续运行,没有则电梯反向。

需要注意的有两点:

  • 如果电梯里有人且当前楼层没有乘客需要下电梯,说明乘客的目标楼层一定在电梯运行方向上的后续楼层,因此本次作业中无需考虑电梯运行到小于1层或大于11层的范围
  • 若请求队列非空且电梯运行方向上没有发出请求的乘客,则说明请求在同层反向或反向楼层。对于同层反向的请求,电梯需要反向运行才能确保接到的乘客都是同层同方向的;而对于位于反向楼层的乘客,电梯需要运行到该乘客的楼层,之后根据乘客的目的方向选择捎带或反向后再捎带。
调度策略类 Strategy

本次作业的调度策略通过Strategy类中的getAdvice方法实现,该方法返回一个枚举类Advide,作为电梯下一步的运行策略。

public synchronized Advice getAdvice(
    int personNum, int maxNum, int posFloor, int direction, 
    RequestQueue requestQueue, HashSet<Person> peopleInEle);
枚举类 Advice

由上述调度策略可知,电梯的运行模式主要有OVER, MOVE, REVERSE, OPEN, WAIT5种,电梯根据getAdvice方法的返回值做出反应:

public synchronized void run() {
        while (true) {
            Advice advice = strategy.getAdvice(id,
                    personNum, posFloor, direction, requestQueue, people);
            if (advice == Advice.OVER) {
                break;
            } else if (advice == Advice.MOVE) {
                move();
            } else if (advice == Advice.REVERSE) {
                direction = direction * -1;
            } else if (advice == Advice.WAIT) {
                requestQueue.waitPerson();
            } else if (advice == Advice.OPEN) {
                open();
                if (requestQueue.getSize() > 0 && maintain != 1) {
                    Person person = null;
                    if (personNum < maxNum) {
                        person = requestQueue.getOnePerson(posFloor, direction);
                    }
                    while (person != null && personNum < maxNum) {
                        in(person);
                        if (personNum < maxNum) {
                            person = requestQueue.getOnePerson(posFloor, direction);
                        }
                    }
                }
                out();
                close();
            }
        }
    }

第二次作业分析

UML类图及协作图

img

img

代码架构分析

本次作业主要关注三个问题:

  • 如何为乘客分配电梯
  • 如何实现电梯的Reset
  • 如何保证Reset前后Receive输出的正确性

电梯分配策略

本人最初选取的策略为所有电梯自由竞争,先到的电梯接取乘客。但由于作业要求每个乘客分配一个指定的电梯,并在电梯移动前输出Receive信息,因此本人在改进Receive输出的过程中遇到了较大困难,最终选择重构。

重构后对于每个请求均匀分配电梯,按电梯编号顺序轮流将请求放入对应电梯的请求序列,具体内容如下:

if (request instanceof PersonRequest) {
    //......
    requestQueues.get(order).addPerson(person, order);
    order += 1;
    if (order > 6) {
        order -= 6;
    }
}

Reset的实现

本人为电梯线程添加了maintain成员,表示电梯是否接收到Reset请求。当输入线程接收到Reset请求时,会将电梯的maintain置为1:

if (request instanceof ResetRequest) {
    //......
    elevators.get(eleId).setMaintain(1, resetRequest.
        getSpeed(), resetRequest.getCapacity());
    requestQueues.get(eleId).reset();
}

电梯每次请求运行策略时,会先判断maintain是否为1,即当前是否需要Reset

public synchronized void run() {
    while (true) {
        if (maintain == 1) {
            if (personNum > 0) {
                open();
                outForMaintain();
                close();
            }
            TimableOutput.println("RESET_BEGIN-" + id);
            reset(resetTime, resetNum);
            TimableOutput.println("RESET_END-" + id);
            requestQueue.resetEnd(id);
            maintain = 0;
            continue;
        }
        //......
    }
}

电梯Reset之前会先放出电梯内的所有乘客。如果乘客的目的楼层为电梯当前所在楼层,则直接将乘客放出;如果不是乘客的目的楼层,则放出的乘客作为新的请求重新加入该电梯对应的请求队列中

public void outForMaintain() {
    while (personNum > 0) {
        for (Person person : people) {
            if (person.getToFloor() == posFloor) {
                //......
            } else {
                //......
                Person newPerson = new Person(person.getId(),
                        posFloor, person.getToFloor());
                requestQueue.addPerson(newPerson, id);
            }
        }
    }
}

Receive的输出

非Reset期间

将乘客加入请求队列时输出Receive信息即可。

Reset期间

根据要求,Reset-Begin输出后到Reset-End输出前,电梯都不可以输出Receive信息,且之前所有未完成的Receive请求均失效。因此在电梯Reset期间分配到的乘客都暂时不输出Receive信息:

public synchronized void addPerson(Person person, int order) {
    if (!isReset) { //elevator not in reset
        TimableOutput.println("RECEIVE-" + person.getId() + "-" + order);
    }
    people.add(person);
    notifyAll();
}

由于每个电梯都分配了对应的请求队列,因此在请求队列中添加了isReset成员,表示电梯是否处于Reset状态。输入线程接收到Reset请求后调用以下方法:

public synchronized void reset() {
    this.isReset = true;
    notifyAll();
}

Reset结束后,电梯调用以下方法,重新Receive未到达目的地的乘客:

public synchronized void resetEnd(int id) {
    for (Person person : people) {
        TimableOutput.println("RECEIVE-" + person.getId() + "-" + id);
    }
    this.isReset = false;
}

第三次作业分析

UML类图及协作图

img

img

代码架构分析

本次作业主要关注三个问题:

  • 如何实现双轿厢电梯
  • 如何实现乘客在双轿厢之间的换乘
  • 如何避免双轿厢之间的碰撞

双轿厢的实现

Reset为双轿厢的重置过程与上一次作业类似,重置后产生两个新的电梯线程,原本的电梯线程结束运行。本人为了避免过多逻辑判断,为A类和B类双轿厢电梯分别设置了DoubleElevatorA类、DoubleElevatorB类和对应的策略类DoubleStrategyADoubleStrategyB。双轿厢电梯的两个轿厢共用原本的请求队列。

对于两类电梯可以接到的乘客(包括输入线程提供的和需要换乘的两类)约束如下:

  • 如果乘客的起始楼层为换乘楼层:运行方向向下的只能由A类电梯接到,运行方向向上的只能由B类电梯接到;
  • 如果乘客的起始楼层低于换乘楼层,则只能由A类电梯接到;
  • 如果乘客的起始楼层高于换乘楼层,则只能由B类电梯接到。

因此对于Receive输出更改如下:

public synchronized void addPerson(Person person, int order) {
    if (!isDouble) {
        if (!isReset) {
            //......
        }
    } else {
        if (person.getFromFloor() == transforFloor) {
            if (person.getDirection() > 0) {
                //...
            } else {
                //...
            }
        } else if (person.getFromFloor() < transforFloor) {
            //...
        } else {
            //...
        }
        people.add(person);
    }
    notifyAll();
}

双轿厢电梯的运行策略也有所更改:

  • 下电梯的乘客不只有到达目的楼层的乘客,还有到达换乘楼层需要换乘的乘客
  • 上电梯的乘客如果处于换乘楼层,则需要判断乘客的目的地是否可达。例如A类电梯只能接收从换乘楼层下楼的乘客;
  • 电梯按原方向继续运行前需要判断是否超过可运行范围;
  • 电梯内没有人,请求队列为空且输入结束时,需要判断另一轿厢中是否还有没到达目的地的乘客。只有另一轿厢中也没有人时才能结束线程,否则无法接到另一轿厢中需要换乘的乘客。
  • 请求队列非空时,需要判断队列中乘客是否为该轿厢可以接到的乘客。例如A类电梯只能接到从换乘楼层以下发出请求的乘客和从换乘楼层下楼的乘客。如果队列中没有可以接到的乘客,则电梯在原位置等待新乘客的加入

对于另一轿厢中是否还有没到达目的地的乘客的判断,本人在RequestQueue中添加了对两轿厢负责乘客数量的统计成员numAnumB。在addPerson分配时增加对应的num,并在换乘时和到达目的地后对该变量进行维护。为了避免线程安全问题,对该变量的读写也都需要加synchronized锁。

如何换乘

当双轿厢电梯到达换乘楼层后,需要先将所有乘客放出。与Reset类似,要判断放出的乘客目的地是否为换乘楼层,如果不是则需要重新放至请求队列中:

public void outForTransfor() {
    while (personNum > 0) {
        for (Person person : people) {
            if (person.getToFloor() == posFloor) {
                //......
                requestQueue.delAPerson(); //此处以A为例
            } else {
                //......
                Person newPerson = new Person(person.getId(), 
                    posFloor, person.getToFloor());
                requestQueue.addPerson(newPerson, id);
                requestQueue.delAPerson(); //此处以A为例
                }
            }
        }
    }

由于前文的接取策略,新放入请求队列的乘客一定会被另一轿厢接取。

如何避免碰撞

本人对于电梯对换乘楼层的访问加锁,进入换乘楼层前请求锁,离开换乘楼层后释放。该过程也在电梯对应的请求队列中实现:

public synchronized void transFor() {
    while (isTransfor) {
        try {
            wait();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    isTransfor = true;
    notifyAll();
}

public synchronized void transforEnd() {
    isTransfor = false;
    notifyAll();
}

当到达换乘楼层的电梯完成乘客的inout等请求后,会按可运行方向运行一层离开换乘楼层,并释放换乘锁。具体内容如下:

if (advice == Advice.MOVE) {
    if ((posFloor + direction) != transforFloor) {
        int lastFloor = posFloor;
        move();
        if (lastFloor == transforFloor) {
            requestQueue.transforEnd();
        }
    } else {
        requestQueue.transFor();
        move();
    }
}
//......
if (advice == Advice.OPEN) {
    //......
    if (posFloor == transforFloor) {
        if ((posFloor + direction) > transforFloor) {
            direction = direction * -1;
        }
        move();
        requestQueue.transforEnd();
    }
}

bug分析

  • 第一次作业中,在策略类出现了bug:向策略类传入的请求队列内容一直没有更新。在getAdvice中添加参数即可解决。
  • 第三次作业中,在Receive输出时出现了问题:电梯接收到Reset请求后便不输出Receive信息,但仍将新分配到的乘客加入请求队列。此时如果电梯还处于上一次开门之后、乘客进入之前的状态,会把尚未Receive但已加入请求队列的乘客get进电梯。修改方式为在乘客进入电梯前添加对于是否接收到Reset请求的判断,若已经接收到Reset请求,则不允许乘客再进入电梯。

心得体会

通过本单元的学习,我对多线程的实现方式有了初步了解。在完成作业的过程中体会到线程安全的重要性,并对同步块和锁的概念有了进一步认识。

...全文
70 回复 打赏 收藏 转发到动态 举报
写回复
用AI写文章
回复
切换为时间正序
请发表友善的回复…
发表回复

301

社区成员

发帖
与我相关
我的任务
社区描述
2023年北航面向对象设计与构造
学习 高校
社区管理员
  • YannaZhang
  • CajZella
  • C_ecelia
加入社区
  • 近7日
  • 近30日
  • 至今
社区公告
暂无公告

试试用AI创作助手写篇文章吧