前言
本单元的任务是模拟多线程实时电梯系统,熟悉线程的创建、运行等操作,了解使用多线程相关的设计模式。要学会解决线程安全问题以及具体Debug方式,保障线程安全的同时实现线程之间的交互。
1.同步块的设置和锁的选择
本单元中我选择的就是Java自带的synchronized
同步块。为了保证锁的唯一性以及防止线程不安全。我让所有电梯、调度器都共享总请求队列,获得总请求队列的锁。在涉及对总请求队列的增、删方法时要加上synchronized
关键字。同时为了尽可能保证效率,能不上锁的地方就不上锁。同时在设置wait
后及时考虑对应的notifyAll
。其实可以使用ReentrantLock
来更加灵活的处理。同时在第七次作业中引入Semaphore
信号量。
要注意处理“轮询”、“死锁”、“非线程安全容器”等问题。
2.调度器设计&策略
头两次作业并没有设置具体的调度方案。就是按照随机分配的方式将请求传入对应电梯。只不过考虑分配时对应人请求与各电梯之间的距离以及对应运行方向。
第三次作业由于电梯可达性的问题,在调度器中设计以个人的起点终点为参数,递归搜索其可换乘电梯的所有路径。在得到所有路径后按照[换乘次数最少] 或者 [换乘次数尽可能少而且其中乘坐的第一部电梯的等待队列也尽可能少] 的判断条件来选择最短路径分配给对应电梯。当电梯在维修或人员换乘出电梯时,所有请求重新回到总请求队列,再由调度器重新分配。
private void searchRoute(Person personRequest, ArrayList<ArrayList<Integer>> routes,
ArrayList<ArrayList<Integer>> paths) {
for (Integer key : elevators.keySet()) {
ArrayList<Integer> floorState = new ArrayList<>(); //0位为curFloor, 1位为destination
int curFloor = personRequest.getFrom();
floorState.add(curFloor);
floorState.add(personRequest.getDesti());
findPath(floorState, elevators.get(key),
new ArrayList<>(), new ArrayList<>(),
paths, routes, new HashMap<>(), new ArrayList<>());
}
}
private void findPath(ArrayList<Integer> floorState, Elevator curEleva,
ArrayList<Integer> currentPath, ArrayList<Integer> currentRoute,
ArrayList<ArrayList<Integer>> allPaths,
ArrayList<ArrayList<Integer>> allRoutes,
HashMap<Integer, Elevator> usedEleva, ArrayList<Integer> usedFloor) {
currentRoute.add(curEleva.getId());
currentPath.add(floorState.get(0));
if (!curEleva.isAccess(floorState.get(0))) {
return;
}
if (curEleva.isAccess(floorState.get(1))) {
allRoutes.add(new ArrayList<>(currentRoute));
currentPath.add(floorState.get(1));
allPaths.add(new ArrayList<>(currentPath));
currentPath.remove(currentPath.size() - 1);
return;
}
synchronized (waitTable) {
usedFloor.add(floorState.get(0));
usedEleva.put(curEleva.getId(), curEleva);
for (int i = 1; i <= 11; i++) {
if (!usedFloor.contains(i) && curEleva.isAccess(i)) {
Iterator iter = elevators.keySet().iterator();
while (iter.hasNext()) {
Integer key = (Integer) iter.next();
Iterator<Integer> integerIterator = usedEleva.keySet().iterator();
int flag1 = 0;
while (integerIterator.hasNext()) {
if (usedEleva.get(integerIterator.next()) == usedEleva.get(key)) {
flag1 = 1;
break;
}
}
if (flag1 == 0) {
floorState.set(0, i);
findPath(floorState, elevators.get(key), currentPath,
currentRoute, allPaths, allRoutes, usedEleva, usedFloor);
currentRoute.remove(currentRoute.size() - 1);
currentPath.remove(currentPath.size() - 1);
}
}
}
}
}
}
3.作业整体架构
整体是按照生产者-消费者模式
设计。输入线程 → 总请求队列 ← 调度器; 调度器 → 各电梯等待队列 ← 电梯线程;总请求队列就是“托盘”,输入线程是“生产者”,调度器是“消费者”。对总请求队列的写入与读取要加同步控制。各个电梯自己有对应的各个等待队列,调度器与各电梯线程之间又构成生产者-消费者模式。
hw5
本次作业相当于多线程的入门,输入线程将读取的请求传入总请求队列waitTable
,因为前两次作业并没有设置调度方案,所以对应的电梯线程直接读取总请求队列并将请求传入自己的等待队列。在电梯的运行过程中执行捎带。同时保证电梯内的人员都是同一运行方向,电梯运行的终点是其中距离最远的人员的目的地。当电梯中没人,等待队列、总请求队列都没人时就进入等待wait()
,当总请求队列中加入请求时执行notifyAll()
;输入关闭且所有人员都送到目的地后线程结束。
电梯按照状态机的方式不断地执行开门、上下乘客、关门、移动的动作。
代码总行数如下:
hw6
本次作业又实现了电梯的增加和删减(维修)功能。要注意线程的关闭和线程的安全。在hw5的基础上,对创建电梯方法进行扩展,对于维修的电梯,在最近的一层停止,电梯内人员出电梯,若未到目的地就修改人员请求的fromFloor
并把该请求传回总请求队列。同时将维修电梯的等待队列清空,等待队列里的所有请求也全部返回总请求队列等待新的分配。
当输入线程关闭且没有维修的电梯且所有人员到达目的地时线程才会关闭。
代码行数如下:
hw7
本次作业添加了对电梯可达性以及每层可开门电梯数量的限制。对于每层可开门数量,利用课程中所讲的信号量semaphore
可以比较方便的实现。重点是实现人员的换乘并保证效率尽可能高。通过写一个递归DFS搜索的算法来实现对一个人员请求的所有路径的遍历与求得。在选择最短路径的过程中既要考虑换乘次数尽可能少,又要保证换乘电梯的人员压力尽可能小。在取得平衡的点上进行人员对电梯的分配。
当输入线程关闭、没有电梯维修且没有人还需要换乘时整个线程关闭。
代码行数如下:
复杂度本身也说明我对于调度器类的实现并没有做到很好的低耦合。
UML类图
三次作业没有进行重构,总体上就是一步步的迭代和小的修改。其中要注意分配人员RTLE
问题,不要将所有人员堆积在某几部电梯上,同时要注意线程安全,尽可能不要使用线程不安全的容器以避免“死锁”。但同时也要考虑效率问题,不能直接全部包在同步控制块内导致并发特点难以发挥。
最终类图如下:
InputThread作为输入线程,充当“生产者”,将输入的请求传给总请求队列WaitTable。调度器Controller作为“消费者”取出请求,按照规划好的调度策略将其分配给各电梯线程Elevator。我将课程组给定的PersonRequest类重新进行了一下封装,将其加入乘客短期目的地的属性(便于换乘)来作为Person类。
UML协作图
迭代—稳定&易变内容
稳定内容:
整体的架构如:输入线程、调度器、电梯线程本身的运行在迭代中基本没有变化。根据作业的需要无非是为电梯增添几个成员属性以及对应的运行状态。
易变内容:
调度器的具体策略、线程关闭条件。由于电梯可达性的限制,导致调度器在分配乘客请求时需要考虑换乘以及其他可能影响性能的因素,导致在迭代中需要对具体的调度策略进行重新编写。实际上可以实现一个策略接口,让不同电梯根据其自身的特点来实现不同的策略,方便扩展。
对于线程关闭条件,实际上只要保证输入关闭,且将所有运行中的乘员存入一个HashMap中,当到达目的地就将其删去,这样只要再判断无剩余乘员线程就关闭。防止出现如输入关闭,电梯维修将人员放出而此时其他所有电梯都已关闭的情况。
4.bug分析&debug
RTLE
:现实时间超时。实际就是运行太慢或者因为出现线程不安全导致的线程未及时结束。对于运行太慢极有可能是自己的调度方案出现了问题,分配人员不公平导致性能极差。使用线程不安全的容器
:本质上还是会导致RTLE,但这种错误本身很难复现,需要评测机的使用,再根据其程序本身报错来寻找对应行代码的问题。真实逻辑问题
:比如我在将电梯等待队列清空并返回总请求队列时将返回的请求人员的fromFloor
换成了当时电梯所在的curFloor
,导致测评WA
。这种行人未到达、电梯未维修等逻辑问题实际上要么是代码本身问题要么就是线程不安全导致的冲突。
Debug过程中对于WA
情况可以在run()
方法中不同位置直接使用println
来在终端查看结果,比较运行过程中出现的数据冲突。同时利用评测机(自己的或其他大佬的)来不停测试自己的程序,一旦出现TLE
就要谨慎对待,绝不能因为多线程的随机性就认为该错误之后不会被测出来。
5.心得体会
-
线程安全
: 多线程设计中需要时刻注意线程安全,避免数据冲突。弄清楚哪些数据需要共享,是否需要读写。在读写时是否要对资源加以保护。同时也不要无脑添加同步块,要考虑效率性能。根据题目条件选择合适的锁或其他原子方法等线程安全的举措。 -
层次化设计
:遵循各种适用的设计模式,让整体架构更加清晰,便于Debug的同时也便于迭代扩展功能。尽量实现“高内聚、低耦合”,不要对一处代码牵一发而动全身。
本单元作业成绩对我来说并不理想,后悔自己没有在强测之前就将可能的Bug解决掉,毕竟最后Debug可能也就缺那几行代码。“悟已往之不谏,知来者之可追。”学习的脚步不会停止,一时的得失决定不了什么,重要的是自己是否在这个学习的过程中真正收获到了知识。希望自己能在接下来的路上能勇敢向前。