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);
}
每次进出电梯后,乘客的fromPosition
和toPosition
将发生改变,若当前位置满足
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
也有自己的问题,就是如果有线程正在读,写线程需要等待度线程释放锁后才能获得写锁,也就是读的过程中不允许写,属于一种悲观的读锁。 -
不得不说,在电梯单元中,收获了比第一单元更多的乐趣(
可能是因为更好玩?)。相比第一单元着力于字符串处理与递归下降的设计,本单元着力于多线程的交互与配合,在线程交互间实现并发,提升程序运行的效率。