OO 第二单元博客作业
第一次作业
题目简述
用多线程实现一个目的选择电梯系统。其中共有五部独立的纵向电梯,请求的出发点和目标点必定位于同一楼座
-
架构分析:由于本次作业请求类型单一,且电梯数目确定,未使用调度器处理请求
-
MainClass:负责电梯的创建和输入线程的启动
-
InputThread:接收请求的输入,并向不同楼座的请求队列分配请求
-
Elevator:内置 look 方法,每部电梯处理自己的请求
-
RequestQueue:存储来自 InputThread 分配的请求
-
PeopleQueue:每部电梯中都有一个,存储当前已经位于电梯上的请求,CAPACITY 设置为 6
-
OutputThread:使用单例模式保证输出顺序正确
-
-
可扩展性分析
-
本次作业中锁与同步块的设置后两次作业中也适用
-
建立类 PeopleQueue 和 RequestQueue,后续增加新属性时较方便
-
同步块与锁
后两次与第一次差别不大,仅在此处说明
-
同步块设置
-
对 RequestQueue 设置了同步,确保其线程安全
-
线程安全类 RequestQueue
-
其中大部分方法采用了 synchronized 限定,保证读写同步
-
采用线程安全容器 Vector 存放 PersonRequest
-
-
-
-
锁的选择
-
采用了
synchronized
,并使用wait-notifyAll
避免轮询
-
-
代码分析
// in Elevator.run() if (requestQueue.isEnd() && requestQueue.isEmpty() && peopleOn.isEmpty()) { return; } synchronized (requestQueue) { if (requestQueue.isEmpty() && peopleOn.isEmpty()) { try { requestQueue.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } } if (requestQueue.isEmpty() && peopleOn.isEmpty()) { return; }
-
为 requestQueue 设置同步,当
requestQueue.isEmpty() && peopleOn.isEmpty()
,使用 wait() 避免轮询;否则表示还有请求尚未处理,继续执行 look 方法进行决策-
为什么 wait 结束还要判断是否为空?
-
InputThread 接收到 null 时,表示请求输入已经结束,
requestQueue.put(request)
中有notifyAll()
会唤醒所有 wait 的电梯线程 -
不加入判断会导致被唤醒的电梯执行一次无用的 look
-
逻辑上的优化,对性能影响不大 : )
-
-
-
bug 分析
这次作业是三次作业中最简单的一次,却是强测寄的最惨的一次。PeopleQueue 中虽然设置了常数容量,但是我在 requesQueue.getOn
中并没有加入 peopleOn.size() >= 6
的判断,超载时仍有乘客进电梯。而 PeopleQueue.put
并不能成功把 PersonRequest 加入容器(因为有容量限制),所以导致这部分超载的乘客丢失。
当中测截止后我发现了这个 bug,互测时构造了超载相关的样例,成功 hack
第二次作业
题目变化简述
-
新增横向电梯,新增创建电梯的请求
-
所有请求的出发点和目标点楼座和楼层必有其一相同
UML图
-
架构分析:由于请求类型不同,需要新增调度器类。与第一次相比的修改如下
-
MainClass:修改为负责调用
Controller.initial()
初始化,和InputThread
的启动 -
InputThread:不再负责请求的分配,只负责接收请求
-
Elevator/FloorElevator:新增横向电梯类,加入 building 属性(之前 building 与 id 存在线性关系)
-
Controller:在 ”调度器设计“ 部分说明
-
-
可扩展性分析:
-
新增 Controller,使用单例模式,便于下一次作业对换乘请求的处理
-
满足定制化需求:
-
Elevator/FloorElevator 仍可以新增属性,如速度、换乘信息等
-
PeopleQueue 可以修改容量设置
-
-
调度器设计
代码分析
public class Controller { private static final Controller CONTROLLER = new Controller(); private ArrayList<RequestQueue> buildingQueues; private ArrayList<RequestQueue> floorQueues; // 调度器单例 public static Controller getInstance(); // 初始化 public void initial(); // 设置结束符 public void setEnd(); // 处理不同请求 public void addBuildingRequest(PersonRequest personRequest); public void addFloorRequest(PersonRequest personRequest); public void addElevator(ElevatorRequest elevatorRequest); public void addFloorElevator(ElevatorRequest elevatorRequest); }
-
initial 方法用于创建所有请求队列和初始的五部电梯
-
setEnd 用于设置结束符,作为判断电梯线程结束的一个条件
-
处理请求的方法
-
人员请求,以
addBuildingRequest
为例 :public void addBuildingRequest(PersonRequest personRequest) { int target = personRequest.getFromBuilding() - 'A'; buildingQueues.get(target).put(personRequest); }
其中 target 表示 personRequest 应该存放到的请求队列的序号
-
电梯请求,以
addFloorElevator
为例:public void addFloorElevator(ElevatorRequest elevatorRequest) { int id = elevatorRequest.getElevatorId(); int target = elevatorRequest.getFloor() - 1; int floor = elevatorRequest.getFloor(); FloorElevator floorElevator = new FloorElevator(id, floor, floorQueues.get(target)); floorElevator.start(); }
该方法根据电梯请求的一些属性,创建新的电梯线程并启动
-
与线程交互
主要利用了单例模式,避免了把调度器作为属性传来传去
-
InputThread
,其run
方法如下:public void run() { while (true) { Request request = elevatorInput.nextRequest(); if (request == null) { // 调用 Controller.getInstance.setEnd() 并退出 } else { if (request instanceof PersonRequest) { // 根据人员请求的横/纵向, 调用 Controller.getInstance.addxxxxRequest(request); } else if (request instanceof ElevatorRequest) { // 根据电梯请求的电梯类型, 调用 Controller.getInstance.addxxxxElevator(request); } } } // elevatorInput.close() }
-
这样做有一定的弊端,即 InputThread 需要支持 request 类型的判断
-
最好把判断加入到 Controller 中,InputThread 只负责把请求传递给 Controller:这是笔者于第三次作业中的实现方式
-
-
MainClass
,其需要调用 Controller 的 initial 方法进行初始化public class MainClass { public static void main(String[] args) { TimableOutput.initStartTimestamp(); Controller.getInstance().initial(); InputThread inputThread = new InputThread(); inputThread.start(); } }
bug 分析
本地测了几组样例保证逻辑正确之后,用了上一次作业bug修复发现没问题,交上去一遍过了就直接开始摆烂
结果强测还是寄了一个点,妹想到是输出顺序不对,有一处输出忘记修改单例了
就 1 行的 bug 修复属实给自己都整无语了orz。互测时随便交了几组较复杂的样例,成功 hack 到。互测结束发现被 hack 到的全是输出顺序问题......
第三次作业
题目变化简述
-
新增换乘请求:出发点和目标点楼层和楼座可以均不同
-
电梯定制化:电梯请求中容量,速度等可变
UML类图
-
架构分析
-
Elevator/FloorElevator:新增定制化信息
-
RequestCounter:采用类似exp4_2的实现方式,用于判断所有任务是否完成
-
为什么需要新增该类?
-
只采用前两次作业中
request == null
就设定终止符不合理 -
之前不涉及换乘,一个请求分配到请求队列中一次就必定会完成
-
本次作业中仍采用之前的方式,会导致被换乘的电梯线程被终止,换乘请求无法完成
-
-
-
RequestQueue 中新增 END_REQUEST 常量,用于唤醒线程
public synchronized void put(PersonRequest request) { if (!Objects.equals(request, END_REQUEST)) { requests.add(request); } notifyAll(); }
-
新增枚举类 ElevatorType,便于分电梯种类划分请求
-
UML顺序图
调度器设计
代码分析
public class Controller { private static final Controller CONTROLLER = new Controller(); private ArrayList<RequestQueue> buildingQueues; private ArrayList<RequestQueue> floorQueues; private ArrayList<FloorElevator> floorElevators; private boolean endTag; public static Controller getInstance(); public void initial(); // 处理换乘信息 public boolean isAccessible(int m, char p, char q); public boolean noNeedSwitch(int floor, char fromBuilding, char toBuilding); public int getSwitchFloor(PersonRequest personRequest); // 处理线程终止 public boolean isEnd(); public void setEnd(); public void notifyAllElevators(); // 处理不同请求 public void dealRequest(Request request); public void addBuildingRequest(PersonRequest personRequest); public void addFloorRequest(PersonRequest personRequest); public void addElevator(ElevatorRequest elevatorRequest); public void addFloorElevator(ElevatorRequest elevatorRequest); }
对换乘信息的处理方法
对于 from 和 to 楼座楼层均不同的请求:
-
当不存在可直达目标楼座的横向电梯(用
noNeedSwitch
判断),Controller 分请求时分给出发楼座的纵向电梯-
因为纵向电梯一定存在且可以到达任一楼层
-
-
先由纵向电梯接到请求,如需换乘,请求改出发信息,作为新请求加入到换乘楼层的请求队列
-
2-FROM-A-4-TO-C-3
-
对于上面请求如果未加其他横向电梯,则路径为 A(4)->A(1)->C(1)->C(3);当到达A(1)时,请求更改为
1-FROM-A-1-TO-C-3
,并由Controller给A(1)的横向电梯;之后即1-FROM-C-1-TO-C-3
给 C 座的纵向电梯 -
每次在look中,遍历peopleOn时,看该层是否存在横向电梯可达该层和目标层。如果存在,请求离开电梯 -> 改请求 -> 换请求队列 -> 进横向电梯 -> ...
-
-
进了横向电梯就不用考虑第二次换乘,到目标楼座就下
-
换乘结束后,请求离开电梯 -> 改请求 -> 换请求队列 -> 进纵向电梯 -> 运行至请求结束
bug 分析
-
课下自己debug时候修改了开门判断的逻辑,但是只判断开门逻辑是不够的
-
对于横向电梯,我采用的是同层自由竞争的模式。由于我电梯上的人员队列类中并没有记录电梯的种类,且该类与第六次作业相比并没有改动,会导致部分换乘请求不能正常完成:
-
只判断开门条件会导致对于某个请求可以开门,而对另一个与其初始位置相同但此横向电梯并不能停靠其目标楼座的请求,就会导致其只能上去但是下不来
-
修改
-
look策略中关于requestQueue的第二次遍历忘记修改了to
-
因为需要换乘,对于当前队列中的to并不应该是最终的to,而可能是换乘的位置。此处与第一次遍历保持一致即可
-
-
对于RequestQueue的getOn方法,需要提供电梯种类和switchInfo进行判断,修改如下:
if (type == ElevatorType.FLOOR_ELEVATOR && (!Controller.getInstance().isAccessible( switchInfo, request.getFromBuilding(), request.getToBuilding()))) { continue; }
即对于横向电梯(因为只有横向会有不能停靠一说),需要判断该请求的目标楼座和出发楼座是否都可达,这样就避免了不能下去的人上来从而导致RTLE的情况
心得体会
从开始连策略都想不明白,到最后线程安全没出过大问题(主要是逻辑上的小瑕疵),笔者在本单元确实收获了很多知识。对于多线程的一些概念,如锁,同步,线程池,线程安全类,死锁和轮询等都有了一定程度的理解。
可是强测作业的得分总是不符合自己的心理预期orz,我认为这很大程度上与我大一以来面向评测机编程的习惯有关。本单元中测强度都很弱,听说有的同学第三次作业没考虑横向电梯开关门也可以顺利通过中测。我一般顺利通过中测(比如一次过)之后就不在自行测试了。对很多细节上的东西缺少关注,出了一些莫名其妙的bug导致强测失分,心里也不是很舒服。