OO_UNIT_2 总结

OO_UNIT_2 总结

HW5

需求分析

实现基本的电梯运行模拟,电梯种类单一(有且仅有纵向电梯),速度统一,载客容量统一,不可新增电梯;乘客需求,局限于同一楼座不同楼层,不存在跨楼座需求。

架构设计

1.乘客:Passenger

    //  乘客id
    private Integer id;
    //  乘客起始座
    private String fromBuilding;
    //  乘客起始楼层
    private Integer fromFloor;
    //  乘客终点座
    private String toBuilding;
    //  乘客终点楼层
    private Integer toFloor;

由于需求较为单一,乘客Passenger类仅仅充当信息保存的容器。

2.电梯:Elevator

电梯组成构造剖析
  • 内部队列(即当前电梯中的乘客)

        private TreeSet<Passenger> innerContainer;
    
  • 外部队列(电梯的任务队列,即电梯还需要接送的乘客集合)

        private TreeSet<Passenger> outerContainer;
    
电梯运行解析:类状态机设计
  • 电梯初始化:内外队列均为空,电梯方向默认上行

  • 1.判断是否需要开门:判断时需要获得电梯对象锁,从而同步当前状态;若需要开门,则先等待400ms(即开关门总计时间),开门后,先进行下客,然后从外部队列中拉取乘客并放入内部队列。

    public synchronized boolean needOpen() {
            if (innerContainer.stream().anyMatch(item -> item.getToFloor() == curFloor)) {
                return true;
            }
            //  若无下电梯的乘客
            return innerContainer.size() < CAPACITY &&
                    outerContainer.stream().anyMatch(item -> item.getFromFloor() == curFloor);
        }
    
  • 2.获取电梯下一步状态:

    //	电梯状态集合    
    public static final Integer UP = 1;
    public static final Integer DOWN = -1;
    public static final Integer WAITING = 0;
    public static final Integer OVER = 2;
    //	判断下一状态的关键方法
    public synchronized Integer nextState() {
            //  若两个等待队列皆空且输入线程结束,则结束电梯线程
            if (innerContainer.isEmpty() &&
                    outerContainer.isEmpty() &&
                    InputThread.getInstance().isOver()) {
                return OVER;
            }
            //  若仅两个等待队列为空,电梯进入等待状态
            if (innerContainer.isEmpty() && outerContainer.isEmpty()) {
                return WAITING;
            }
        	//	若当前为上行,则判断当前方向上是否还有乘客有开门需求
            if (direction == UP) {
                Integer maxFloor = getMaxFloor();
                if (maxFloor >= curFloor && curFloor < MAX_FLOOR) {
                    return UP;
                }
                else {
                    return DOWN;
                }
            }
        	//	若为下行,逻辑同上行
            else (direction == DOWN) {
                Integer minFloor = getMinFloor();
                if (minFloor <= curFloor && curFloor > MIN_FLOOR) {
                    return DOWN;
                }
                else {
                    return UP;
                }
            }
        }
    ...
        Integer nextDirection = nextState();
    
  • 3.电梯按获得的nextDirection运动一次,回到第一步

3.输入模块:InputThread

  • 单例模式
    private static InputThread instance = new InputThread();

    private InputThread() {}

    public static InputThread getInstance() {return instance;}
  • 实时接收输入,同时也是电梯offerRequest(Passenger passenger)方法的唯一调用者
  • 输入结束后陷入OVER状态,同时等待所有电梯线程结束

UML类图

HW6

需求分析

在第一次作业的基础上增加了横向电梯,增加了增加电梯的请求;乘客需求,不再局限于同一楼座,但仍然只需一次运送即可到达目的地(不需要换乘)。

架构设计

调度策略:随机分配

1.乘客:Passenger

与HW5设计无异,此处不再赘述

2.电梯:Elevator

考虑到增加了横向电梯,故采用继承与接口的方式在HW5的基础上进行了增量迭代,依托关系如下

其中,horElevator (横向电梯)与verElevator(纵向电梯)主要区别在于移动方式:horElevator 一经初始化方向则不再改变,初始方向的确定则采用左右轮流的随机方式;verElevator则沿袭了HW5的状态机设计:每次移动一层,等待下一个状态。

3.输入模块:InputThread

HW6增加了电梯请求,此请求在输入线程中完成

addPersonRequest(PersonRequest)addElevatorRuest(ElevatorRequest)方法分别完成新增乘客与电梯的请求。在增加乘客请求后,将开启一个分配线程,选择合适的电梯,将乘客直接投送至该电梯的外部队列中(即调用电梯的offerRequest方法)。

4.分配器:Allocator

public class Allocator implements Runnable {
    //  待投放的乘客
    private Passenger passenger;
    //  电梯容器
    private ArrayList<Elevator> elevatorList;

    public Allocator(Passenger passenger, ArrayList<Elevator> elevatorList) {
        this.passenger = passenger;
        this.elevatorList = elevatorList;
    }

    @Override
    public void run() {
        /*使用随机策略,随机从电梯池中选出一个电梯,避免阻塞等待,
        引起java.util.ConcurrentModificationException异常.*/
        Elevator elevator = elevatorList.get(new Random().nextInt(elevatorList.size()));
        elevator.offerRequest(passenger);
        InputThread.getInstance().delAllocator();
        synchronized (InputThread.getInstance()) {
            InputThread.getInstance().notifyAll();
        }
    }
}

考虑到分配调度的复杂性,本次采用随机策略:分配器在确认对应楼层与楼座后,在符合要求的电梯中使用随机数随机选出一个电梯,然后投送。

UML类图

BUG分析

初次架构时,Allocator未使用随机策略,而是采用了“空电梯优先,人少者优先”的策略,选取符合条件的第一个电梯进行投送,然而在选择电梯时,由于在迭代中选择电梯同时执行电梯方法改变电梯状态,引发java.util.ConcurrentModificationException异常(到目前为止也没完全搞明白);然而在修复BUG的对比中发现,两次性能差距微乎其微,甚至大部分测试中随机分配反而更快,于是乎选择了随机策略。

HW7

需求分析

在前两次作业的基础上有了较大幅度的改变:电梯增加了定制功能(即指定电梯容量、速度),且横向电梯增加了可开门选择的属性(即不一定在每一个楼座均能开门);同时,乘客需求进一步复杂化,起点和终点除了基本的范围限制(楼座限制、楼层限制)以及不能相同外不再有其他限制,如此便使得每次乘客的请求都可能需要拆分,即需要考虑换乘。

架构设计

调度策略:分配器分配,遵循“空电梯优先,人少者优先,快者优先”规则

1.乘客:Passenger

    //  乘客id
    private final Integer id;
    //  乘客目前起始位置
    private Position fromPosition;
    //  乘客下一次到达位置
    private Position toPosition;
    //  乘客起点
    private final Position beginPosition;
    //  乘客终点
    private final Position finalPosition;
    //  乘客的中转站集合
    private final Queue<Position> transferStations;
    //  是否到达终点
    private boolean isArrived;

考虑到中转的需要,于乘客增加了中转的集合:即每个乘客都有自己的路线,电梯只通过乘客的如下方法判断是否有乘客进出电梯

    //	是否需要出电梯
public boolean needOut(Position curPosition) {
        return curPosition.equals(toPosition);
    }
	//	是否需要上电梯
public boolean needIn(Position curPosition) {
        return curPosition.equals(fromPosition);
    }

每次进出电梯后,乘客的fromPositiontoPosition将发生改变,若当前位置满足

    public boolean arrivedAtDestination(Position curPosition) {
        return curPosition.equals(finalPosition);
    }

后,该乘客即到达目的地;否则,将默认乘客需要换乘,通过乘客出当前电梯时增加判断

for (Passenger passenger : tmpSet) {
                //  若未到达终点
                if (!passenger.arrivedAtDestination(curPosition) &&
                        !passenger.getTransferStations().isEmpty()) {
                    //  乘客更换目的地
                    passenger.setFromPosition(passenger.getToPosition());
                    passenger.setToPosition(passenger.getTransferStations().poll());
                    //  乘客重新分配
                    Allocator.getInstance().addNewRequest(passenger);
                }
                else if (passenger.arrivedAtDestination(curPosition)) {
                    passenger.setArrived(true);
                    synchronized (InputThread.getInputThread()) {
                        InputThread.getInputThread().notifyAll();
                    }
                }
            }

乘客的路径规划来源于当前各楼层电梯节点连通状态,详见2.节点:Node

2.节点:Node

将每个可达位置视为一个节点,则所有节点通过电梯的连通,自然而然形成了一个不带权的无向连通图。通过Queue<Position> getPath(Node b)方法,获取当前位置到目的地的路径序列,以队列的形式返回。具体实现如下:

public Queue<Position> getPath(Node b) {
        if (position.equals(b.getPosition())) {
            return new LinkedList<>();
        }
        Queue<Node> nodeQueue = new LinkedList<>();
        Stack<Position> positionStack = new Stack<>();
        nodeQueue.offer(this);
        this.setVisited(true);
    	//	使用广度优先搜索寻找最短路径
        while (!nodeQueue.isEmpty()) {
            Node node = nodeQueue.poll();
            if (node.equals(b)) {
                positionStack.push(node.getPosition());
                while (node.getPreNode() != null) {
                    node = node.getPreNode();
                    if (!node.equals(this)) {
                        positionStack.push(node.getPosition());
                    }
                }
                break;
            }
            else {
                for (Node item : node.getNeighborSet()) {
                    if (!item.isVisited()) {
                        nodeQueue.offer(item);
                        item.preNode = node;
                        item.setVisited(true);
                    }
                }
            }
        }
        InputThread.getInputThread().cleanNode();//	清除所有访问标记
        Queue<Position> positions = new LinkedList<>();
    	//	解析路径
        Position lastPosition = positionStack.pop();
        boolean lastDirection = getDirection(this.position,lastPosition);
        while (!positionStack.isEmpty()) {
            Position position = positionStack.pop();
            boolean curDirection = getDirection(position,lastPosition);
            if (!curDirection || (!lastDirection)) {
                positions.add(lastPosition);
            }
            lastDirection = curDirection;
            lastPosition = position;
        }
        positions.add(lastPosition);
        return positions;
    }

3.电梯:Elevator

本次电梯同HW6的设计及其相似,仅仅只是横向电梯有了可开门限制

    public boolean canOpenAt(Position position) {
        int m = openKey;
        char x = position.getBuildingStr().charAt(0);
        return ((m >> (Character.compare(x,'A'))) & 1) == 1 &&
                position.getCurFloor().equals(curPosition.getCurFloor());
    }

其余不予赘述。

4.输入模块:InputThread

  • HW7中,考虑到哈希表查询的便捷性不明显,存放电梯的数据结构不再使用HashMap,而是单纯使用列表容器ArrayList

  • 增加乘客id - 乘客的哈希映射表passengerMap,用以判断是否所有乘客都到达了目的地,从而结束主线程

  • 增加横向电梯时,将进行楼层连通图的更新

    public void updateNodeMap(Elevator elevator) {
            Integer floor = elevator.getCurPosition().getCurFloor();
        	//	若新电梯在一层,则无需更新
            if (floor.equals(1)) {
                return;
            }
        	//	更新过程,nodeHashMap将被锁定
            synchronized (nodeHashMap) {
                ArrayList<Position> positions = new ArrayList<>();
                for (char ch : BUILDINGS) {
                    String building = String.valueOf(ch);
                    Position position = new Position(building, floor);
                    if (elevator.canOpenAt(position)) {
                        positions.add(position);
                    }
                }
                //	可达楼座两两进行连通
                for (Position position1 : positions) {
                    for (Position position2 : positions) {
                        if (!position1.equals(position2)) {
                            Node node1 = nodeHashMap.get(position1);
                            Node node2 = nodeHashMap.get(position2);
                            node1.addLinkWith(node2);
                            node2.addLinkWith(node1);
                        }
                    }
                }
                nodeHashMap.notifyAll();
            }
        }
    

    输入模块属性一览

    //  输入是否结束
    private Boolean isOver;
    //  楼层连通图
    private final HashMap<Position, Node> nodeHashMap;
    //  单例
    private static final InputThread INPUT_THREAD = new InputThread();
    //  乘客表
    private final HashMap<String, Passenger> passengerHashMap;
    //  电梯容器
    private final ElevatorContainer elevatorContainer;
    //  空电梯表
    private final WaitElevatorList waitElevatorList;
    private final Integer[] flags = new Integer[11];

4.空电梯等待容器(只包含等待状态中的电梯):WaitElevatorList

  • 顾名思义,等待中的电梯均集中于此处

  • 根据乘客需求,选择符合条件的等待中电梯;若无符合要求者,返回null,进一步从所有电梯容器中进行选择

        public synchronized Elevator getWaitElevator(Passenger passenger) {
            Elevator tar;
            if (passenger.isVertical()) {
                tar = waitList.stream().
                        filter(item -> item.isVertical() &&
                                item.compareBuilding(passenger.getFromPosition())).
                        findFirst().orElse(null);
            }
            else {
                tar = waitList.stream().
                        filter(item -> !item.isVertical() &&
                                item.canOpenAt(passenger.getFromPosition()) &&
                                item.canOpenAt(passenger.getToPosition())).findFirst().
                        orElse(null);
            }
            if (tar != null) {
                waitList.remove(tar);
            }
            return tar;
        }
    

5.电梯容器(包含所有电梯):ElevatorContainer

  • 存放当前时刻所有电梯

  • 根据乘客起始地以及目的地选出最合适的电梯并返回;采用HW6未能成功实现的“空电梯优先,人少者优先”策略,同时,纵向电梯增加“速度快者优先”的策略,而横向电梯在基本策略基础上使用随机选择的方式进行分配。

public synchronized Elevator getValidElevator(Passenger passenger) {
        if (passenger.isVertical()) {
            ArrayList<Elevator> validList = elevatorList.parallelStream().
                    filter(item -> item.isVertical()
                          &&  item.compareBuilding(passenger.getToPosition())).
                    collect(Collectors.toCollection(ArrayList::new));
            Elevator quickest = validList.parallelStream().
                    filter(item -> item.getSpeed().equals(Elevator.QUICK_SPEED)).
                    findFirst().orElse(null);
            if (quickest != null) {
                return quickest;
            }
            Elevator normal = validList.parallelStream().
                    filter(item -> item.getSpeed().equals(Elevator.NORMAL_SPEED)).
                    findFirst().orElse(null);
            if (normal != null) {
                return normal;
            }
            return validList.get(new Random().nextInt(validList.size()));
        }
        //  横向电梯不采用优化策略
        ArrayList<Elevator> validList = elevatorList.parallelStream().
                filter(item -> !item.isVertical()
                        &&  item.canOpenAt(passenger.getFromPosition()) &&
                        item.canOpenAt(passenger.getToPosition())).
                collect(Collectors.toCollection(ArrayList::new));
        return validList.get(new Random().nextInt(validList.size()));
    }

6.分配器:Allocator

  • 在HW6的基础上进行优化,不再使用Thread-Per-Message Pattern (一乘客一线程)的方式进行分配,而是使用Producer-Consumer Pattern(生产者-消费者模式),采用内置线程安全的BlockingQueue盛放请求。
    private static final Allocator ALLOCATOR = new Allocator();
    private final BlockingQueue<Passenger> passengerQueue;	//	请求队列
  • 运行时,采用“空电梯优先,人少者优先”策略进行分配,具体实现如下:
public void run() {
        while (!InputThread.getInputThread().gameOver()) {
            Passenger passenger = passengerQueue.poll();
            if (passenger == null) {
                try {
                    synchronized (this) {
                        wait();
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            else {
                //  先找空电梯
                Elevator elevator;
                elevator = InputThread.getInputThread().getWaitElevatorList().
                        getWaitElevator(passenger);
                if (elevator == null) {
                    elevator = InputThread.getInputThread().getValidElevator(passenger);
                }
                elevator.offerRequest(passenger);
            }
        }
    }

UML类图

时序图

  • 主线程

  • 输入线程

  • 分配器线程

  • 纵向电梯线程

  • 横向电梯线程

BUG分析

本次bug出在路径生成部分,未考虑到横向电梯开门的限制,从而导致路径出错,进而使得分配电梯时找不到符合要求的电梯。

解决方式:修改getPath方法,考虑横向电梯的开关门限制即可。

心得体会

  • 先前未有过系统学习并发编程,经过三次电梯作业的训练,对并发编程有了更进一步的认识,熟练度进一步加深

  • Java 提供的并发组件,大致可以分为两类:

    从预防阶段下手,防止错误发生,比如说 synchronized 关键字
    一旦发生错误能及时重试,比如说 CAS
    对于线程数量比较多的并发场景,采用预防的措施会比较合理,这样大部分线程就不会因为小概率时间的 CAS 重试浪费掉大量的 CPU 周期;在线程数量小的时候,CAS 的意义就比较大,因为预防措施带来的线程切换要比 CAS 等待的开销更大。

  • 并发编程可以抽象成三个核心问题:分工、同步和互斥。

    1)分工

    分工指的是如何高效地拆解任务并分配给线程,像并发编程领域的一些设计模式,比如说生产者与消费者就是用来进行分工的。

    2)同步

    同步指的是线程之间如何协作,一个线程执行完了一个任务,要通知另外一个线程开工。还拿生产者-消费者模型来说吧,当队列满的时候,生产者线程等待,当队列不满的时候,生产者线程需要被唤醒重新执行;当队列空的时候,消费者线程开始等待,不空的时候,消费者线程被重新唤醒。

    3)互斥

    互斥指的是保证同一时刻只有一个线程访问共享资源,是解决线程安全问题的杀手锏。

    当多个线程同时访问一个共享变量的时候,很容易出现“线程安全”问题,因为结果可能是不确定的——导致出现这个问题的根源就是可见性、有序性和原子性——为了解决它们,Java 引入了内存模型的概念,可以在一定程度上缓解“线程安全”的问题,但要想完全解决“线程安全”问题,还得靠互斥。

    互斥的核心技术就是锁,比如说 synchronized,还有各种 `Lock。

    锁可以解决线程安全的问题,但同时也就意味着程序的性能要受到影响。

    因此,Java 提供了针对不同场景下的锁,比如说读写锁 ReadWriteLock,可以解决多线程同时读,但只有一个线程能写的问题;但 ReadWriteLock 也有自己的问题,就是如果有线程正在读,写线程需要等待度线程释放锁后才能获得写锁,也就是读的过程中不允许写,属于一种悲观的读锁。

  • 不得不说,在电梯单元中,收获了比第一单元更多的乐趣(可能是因为更好玩?)。相比第一单元着力于字符串处理与递归下降的设计,本单元着力于多线程的交互与配合,在线程交互间实现并发,提升程序运行的效率。

posted @ 2022-04-27 22:17  不怕事学渣扛把子势力  阅读(71)  评论(0编辑  收藏  举报