BUAA OO2023第二单元总结

问题描述

对一个十一层的楼层系统设计 6 台并行的电梯,完成实时输入的乘客的请求

电梯系统满足:捎带乘客,添加电梯,维修电梯,可达性判断

由于电梯的可达性限制,不能够乘坐一部电梯到达的乘客需要经过换乘电梯才能到达相应的楼层


度量分析

1. UML 图

在这里插入图片描述

2. 复杂度分析

1. 类复杂度

在这里插入图片描述

2. 方法复杂度

  • EleThread

在这里插入图片描述

  • Scheduler

在这里插入图片描述

3. 行数统计

在这里插入图片描述


三次迭代架构简介

hw5

1. 题目描述

在十一层的楼房中建立 6 个有最大容量的电梯,电梯支持捎带,电梯的满载数为 6 ,可以到达所有的楼层

2. 生产者-消费者模型

在我的设计中,building[] 数组作为生产者消费者模型的托盘,声明为 private static final Floor[] building = new Floor[12] ,每一个 floor 元素是一个 passenger 队列(用 ArrayList实现),即为等待队列,用每一个 building[floor] 代指每一层,即共有 11 个等待队列。

elevator 可以访问 building[] 进行乘客的上下电梯,楼层的扫描等操作

MainThread InputThread building[floor] ElevatorThread Scheduler start input passenger request awake elevator use tools to scan and fetch fetch passenger piggyback passenger !canFetch -- elevator wait input end input end Thread end Thread end !canFetch Thread end MainThread InputThread building[floor] ElevatorThread Scheduler

3. 调度策略

在笔者的设计中,电梯是一个具有自主决策的 “智能生物体” ,电梯通过外部进行唤醒 (notify),可以自行对楼层进行扫描,判断是否停靠,并自主决定捎带,而调度器 (Scheduler) 笔者将其设置为一个 “工具箱”,即一些函数的集合类,一个非线程类,每个电梯都可以随意地使用 “工具箱” 内的工具进行自己的运行,使用该 “工具” 去访问共享对象的时候对共享对象进行保护不对工具的数量进行限制和不对工具的调用进行保护

笔者将电梯定义为一个状态机,具有四个状态:

STOP_STATUS 停止状态:在此状态中电梯一定为空,判断是否进行 wait 或着进行空电梯的接人

UP_STATUS 上升态:在此状态判断下一层是否停靠,并且判断是否可以捎带

DOWN_STATUS 下降态:在此状态判断下一层是否停靠,并且判断是否可以捎带

IN_OUT-STATUS 进出态:在此状态进行乘客的进出

在计组的状态机中,通常会设置一种 “自旋” 的状态,即对于输入为 q 的时候,status_1 仍变为 status_1 ,这样的设计实现在程序中其实就是轮询,而轮询是一种极度耗费 CPU 资源的操作,体现在评测中会爆 CPU 超时CPU_TIME_LIMIT_EXCEED,所以在电梯空闲的时候一定要进行 wait

由于乘客的到来是动态的,而电梯的运行状态会随着乘客变化,为此,笔者设置了一种 “凭票上电梯” 的机制,即一个乘客若是想上电梯,必须持有该电梯的电梯票,否则即使电梯在你面前开门,你也无权上电梯。进一步抽象电梯与乘客的 binding 机制,上电梯的乘客一定在电梯的 binding 队列中。

最终电梯类的设置如下:

public class Elevator {
    private static final int STOP_STATUS = 0;
    private static final int UP_STATUS = 1;
    private static final int DOWN_STATUS = 2;
    private static final int IN_OUT_STATUS = 3;
    private int floor;                           // 当前电梯所在楼层
    private int status;                          // 当前电梯的状态
    private ArrayList<Passenger> requests;       // 已经在电梯中的乘客
    private ArrayList<Passenger> bindings;       // 已经建立 binding 的乘客,即持有电梯票的乘客
}

public class EleThread extends Thread {          // 电梯线程,每个电梯线程中有一个电梯
    private final Elevator elevator; 
    private final int number;                    // 该电梯线程中的电梯号 (其实也可以放在电梯类中)
    private boolean fetch;                       // 是否处于 fetch 态————空电梯接人状态
}
1. 电梯唤醒策略

为了避免轮询,在第一位乘客到来的时候电梯一定处于 wait 中,需要将电梯进行唤醒并开始运行;此外,在电梯系统已经处理完楼房中的乘客请求,而输入线程并没有结束的时候,电梯需要进入 wait 态,并在下次的乘客请求输入时再次将电梯唤醒。

建立一种空电梯优先的机制,又可以描述为:乘客都有乘坐空电梯的倾向社恐,有空电梯会先乘坐空电梯,没有空电梯了会被捎带或者等待电梯空闲下来。这样的机制对于某些情况下来说会极度耗电,但是对于耗电量的减少笔者并没有进行过多的考虑。

那么在乘客输入时进行统一的电梯唤醒是满足上述要求的,即在每一个乘客的输入和输入线程结束时都进行 awakeAll()

2. 捎带策略

捎带的条件:乘客的目的方向与电梯此刻的运行方向相同,则允许捎带

捎带的判断:电梯在运行 (UP_STATUS DOWN_STATUS ) 的过程中对即将到达的楼层进行扫描,如下一楼层中存在满足捎带条件的乘客,则建立 binding 关系,电梯会在到达下一楼层之后进行停靠并接这个乘客上电梯

3. 空电梯启动策略

在空电梯被唤醒之后,进入空电梯接人态,即 fetch 态,被唤醒的电梯会先对 building 进行扫描,寻找到一个未被 binding 的乘客,建立 binding 关系并进入 fetch 态去接这个乘客,当 fetch 态为 true 时,电梯停止捎带,只有接到这个人之后才允许捎带,接到人之后将 fetch 置为 false

设置空电梯接人时停止捎带是基于这样的考量:考虑情况:

现有:乘客1:from 6 to 1 乘客2:from 5 to 11 现在有一个空电梯处于 3 楼去到 6 楼接乘客1,问题:该电梯要不要捎带乘客2?

根据捎带策略:若乘客的方向与电梯的运行方向相同,且电梯容量未满,允许捎带。

为保证统一性,那该电梯理应捎带乘客2,那乘客1在 6 楼上电梯的时候,不是直接向下行驶了,而是先上到 11 楼将乘客2送达,再从 11 下到 1 楼到达目的地。那其实乘客1 其实是多走了**“冤枉路”**

并且:倘若捎带了乘客2会造成 binding 队列中出现乘客方向不一致的情况,这将会非常麻烦,因为已经无法确定主请求!!

那其实根据上述例子也可以举出一下的例子:

现有:乘客1:from 3 to 11 乘客2:from 5 to 8 现在有一个空电梯处于 2 楼去到 3 楼接乘客1,问题:该电梯要不要捎带乘客2?

很显然,如果捎带了乘客2电梯的速度会快上很多,而不捎带则电梯会多走很多的路…

我感觉很难定义这是一个好的方案还是坏的,总是简单就完事了

4. 电梯退出条件

设置线程安全的全局原子类型变量:

private static final AtomicInteger existUnbinding = new AtomicInteger(0);   // 记录没有被 binding 的个数

用于记录 building[] 中没有被 binding 的乘客数目,当输入线程结束且没有被绑定的乘客了,电梯自然可以退出。

5. bug 分析

1. REAL_TIME_LIMIT_EXCEED

强测的 REAL_TIME 超时主要是因为我起初的调度策略太差了…

因为起初我的设计是扫描电梯即将到达的一层,判断是否可以捎带,并且在 fetch 态中禁止了捎带,这就会造成:当 fetch 态结束(即接到人的那一层)的时候的那一层不能捎带,在强测下行高峰的是时候就会爆 REAL_TIME。修改很简单:加一个方法支持本层捎带就可以了

2. CPU_TMIE_LIMIT_EXCEED

原因分析:

  • 死锁:两个嵌套的 synchronized 是非常容易造成死锁的,并且要谨慎函数类的类锁,也有可能会造成死锁
  • 轮询:没有安全的进入 wait() 甚至我在刚开始不理解轮询的意思的时候都没用wait

hw6

1. 题目描述

新增电梯增加和电梯维护的请求,新加的电梯具有满载人数和移动速度的参数,电梯维护需要在两个 ARRIVE 中进行电梯的维护,且为永久维护,即退出

2. 电梯类设计

对于电梯新增的参数,在电梯类中加上相应的成员,并更改构造器即可

public class Elevator {
    private static final int STOP_STATUS = 0;
    private static final int UP_STATUS = 1;
    private static final int DOWN_STATUS = 2;
    private static final int IN_OUT_STATUS = 3;
    private int floor;
    private int status;
    private int capacity;
    private double speed;
    private ArrayList<Passenger> requests;
    private ArrayList<Passenger> bindings;
}
public class EleThread extends Thread {
    private final Elevator elevator;
    private final int number;
    private boolean fetch;
    private boolean maintain;
}

3. 维修电梯

在电梯线程类中加入 boolen maintain 参数,并在电梯状态机中进行判断,如果进入维修态,尽快的停靠并放出电梯中的所有乘客

注意:官方接口中的 passengerRequest 中没有乘客 fromFloorset 方法,而维修的电梯放出乘客时需要更改乘客的 fromFloor ,可以将原先的乘客销毁,用其中的参数构造一个新的乘客加入相应楼层的等待队列

4. bug 分析

第六次作业强测没有出现 bug,感觉第六次作业的重点是修第五次作业的 bug,增量的部分并不多

hw7

1. 问题描述

新增电梯可达性要求,并用掩码表示

新增楼层服务数最大数的限制,只能同时有 4 部电梯在同一楼层处于服务中,只能同时有 2 部电梯在同一楼层处于只上人状态。

注:在强测中可能会出现 1-6 号电梯均维修,此时,乘客必须乘坐新增的电梯,由于新增电梯的可达性限制,可能会出现乘客必须换乘才能到达目的地的情况

2. 乘客路径规划

规定:乘客都是懒惰的,他们都想乘坐尽量少的电梯,即尽量避免换乘,哪怕需要等较长的时间

乘客类定义如下:

public class Passenger extends PersonRequest { 
    private boolean binding;                                   // 记录该乘客是否已经被电梯 binding
    private boolean oneStepComplete;                           // 判断该乘客是否只需要乘坐一个电梯就可以达到目的地,即是否一步可达
    private ArrayList<Path> paths = new ArrayList<>();         // 若乘客非一步可达,记录该乘客的换乘队列
}

设置全局的电梯信息表,记录电梯的可达信息,并在 ADD 电梯和 Maintian 电梯的时候进行同步

private static final ArrayList<ReachTable> reachabilityTable = new ArrayList<>();

在创建乘客时,判断该乘客是不是"一步可达",否则规划路径,计入 passenger.paths 中,在规划路径中,根据全局的电梯可达信息表实时建立邻接链表,采用广度优先搜索(BFS)的算法进行路径的规划

设置全局电梯可达信息表的好处:

在我的设计中,原则上是电梯线程只与托盘进行交互,其他的线程不要去影响电梯线程(哪怕是从电梯线程中读取数据),这个全局的信息表就是一个托盘,如果不设置这个信息表而只把电梯的可达信息设置在电梯类中,那么每次想要读取到该电梯的可达信息就要拿到这个线程并去访问,有可能会造成线程的相互影响,出现难以预知的错误

当然了,电梯线程内部的电梯类中也是要有可达性信息的,这方便电梯进行自我决策的时候拿到信息进行判断

此外,保证非一步可达的乘客的 paths 路径中的元素个数一定大于 1 ,并在其中每一个路径完成之后实现元素的删除,即删除第 0 号元素,并将非一步可达的乘客最后转化为一步可达

根据标程:对无法使用一部电梯完成的请求采用静态调度策略,即事先规划好完成该请求时需要乘坐的电梯顺序,然后按照该顺序乘坐电梯,当这些电梯中有的电梯在还未乘上便进入维护时则重新规划。

3. 乘客刷新机制

乘客的路径是在一开始就决定了的,但是会存在电梯的增加和维修,这时就需要建立乘客的路径刷新机制

约定:只刷新未 binding 的乘客 至于为什么这么约定稍后会进行解释

在电梯增加和维修时进行刷新,即路径的重新规划,注意:增加和维修之后的刷新需要在对全局的电梯可达信息表更新之后

4. 电梯添加策略

电梯的添加相对简单,创建新的电梯线程并 start 就可以,要记得同步全局电梯可达信息表

电梯添加时刷新乘客的原因:

在该电梯尚未添加时,乘客的路径规划是基于之前的电梯系统的,这就使得该电梯在进入电梯系统中并不能直接开始工作,不对乘客进行刷新会降低效率。

5. 电梯维修策略

电梯的维修较为复杂,考虑一下几种情况:

  • 维修电梯中的乘客,即在 request 队列中的乘客。在电梯维修时需要将该电梯尽快停靠并将这些乘客放出电梯

  • 维修电梯 binding 队列但不在 request 队列中的乘客,即持有了电梯票却还没上电梯的乘客。需要取消这些乘客的 binding 关系,等待刷新(刷新即为重新规划路径)

  • 其他电梯 request 队列中的乘客,并且该乘客的路径中存在被维修的电梯。要尽快地停靠让这些乘客下电梯,取消 binding 关系并刷新乘客路径

  • 其他电梯 binding 队列但不在request 队列中的乘客,并且该乘客的路径中存在被维修的电梯。要尽快地取消该电梯的 binding 关系并刷新乘客路径

  • 未被 binding 的乘客,刷新路径。

设计原则

建立电梯的自决策机制,在电梯自己运行的过程中不断地进行自我扫描和判断,这种自我的扫描和判断是基于托盘中的共享元素,而不是一个电梯线程直接去影响另一个电梯线程。体现在程序中就是:维修的电梯对电梯可达信息表的信息进行删除,其他的电梯在运行的过程之中,不断根据全局的电梯可达信息表进行自我判断,如果存在有 bindingrequest 队列中的乘客的路径已经在电梯可达信息表中消失了,那就尽快停靠将该乘客踢出电梯。

总的来说:电梯进入 IN_OUT_STATUS 的条件有:

  • 是否可以停靠?可达性,同一楼层的限额性

  • 需要踢人

  • 有乘客达到目的地

  • 需要上人

并且在每一个状态中定期的判断 binding 列表中是否存在需要解绑的乘客,若有,则解绑。

6. 捎带与接人

之前的捎带:同方向即可捎带

之前的接人:随便一个未绑定的人

现在的捎带:

  • 一步可达:同方向且可以到达指定楼层
  • 非一步可达:同方向,paths 的第 0 号元素满足电梯号要求

现在的接人:

  • 一步可达:可以一步到达指定楼层
  • 非一步可达:paths 的第 0 号元素满足电梯号要求

关键:非一步可达的乘客已经规划好了路径,根据静态调度策略,必须由规划好的电梯去接这个人或者捎带

7. 楼层限额策略

第七次作业新增要求:

  • 对任意楼层 X X X,处于服务中的电梯的最大电梯数量 M X = 4 M_X = 4 MX=4
  • 对任意楼层 X X X,处于服务中的只接人的电梯的最大数量 N X = 2 N_X = 2 NX=2

设置全局的电梯服务数数组,用于记录正在该楼层服务的电梯数:

private static final ServiceNumber[] serviceNumberTable = new ServiceNumber[12];

public class ServiceNumber {
    private int servicing;
    private int onlyFetch;
}

在我的设计中,电梯到达指定的楼层时,先判断能不能开门,若当前该楼层的服务数已经满了,就让该电梯 sleep(1000) 然后再进入开门判断,若还不满足,继续 sleep

此处的设计初衷应该是想要实现信息量,即使没有信息量也应该是使电梯进入 wait 在合适的时机再唤醒,此处笔者为了偷懒直接用 sleep 了…

8. bug 分析

对于位于其他容器中浅拷贝的元素保护不到位,例如我在电梯的 request 队列中的 passenger 元素进行修改时,没有对存放 passenger 的容器——building 进行保护,而电梯 request 队列中的乘客是由 building 中的乘客浅拷贝而来的,公用一块内存,造成了foreach报错。

更改很简单:加了两个锁

稳定与易变

由于笔者的电梯调度属于自由竞争,每次唤醒全部的电梯去竞争乘客,不同的运行下唤醒的电梯是随机的,这就造成了每次的运行结果都是不同的,bug 也易变,时而报错、时而不结束、时而正常的情况经常发生。

但倘若是电梯覆盖楼层的时候只存在一条路径,那乘客的路径也将是固定的,此时电梯的调度将是稳定的。

心得体会

多线程的 debug 真是艰难!!!

多线程的 debug 真是艰难!!!

多线程的 debug 真是艰难!!!

没有办法设置断点进行调试,错误在本地难以复现,CPU 超时根本体现不出来…自始至终我的 debug 方式就一个字——盯😇

基于程序逻辑的 debug 就要求一定得清楚锁的使用和线程之间的关系!

这里我根据自己的体会写了一篇 《浅析 notify wait 与 synchronized》,里面记录了我整体的思想变化和关键问题,不排除有错误的内容,请批判性的阅读,对于其中的大多数情况我都是自己打一个小的 demo 进行自己测试,我认为这样更有利于对正确性的验证和对整体的把握。

此外,一定要确保自己完全理解了多线程之间的相互关系再进行建模,否则就很有可能进入无限的 debug 中,笔者就与 CPU 超时斗智斗勇了整整三周星期…

“天高地迥,觉宇宙之无穷”,这一单元勉强算是笔者对于多线程的初探,提起多线程我总是想起我同学说的:“如果你认为自己已经学会了多线程,那么说明你还没有真正学会多线程”,将电梯建模为状态机,最后搓出来将近 200 行的 run 函数属实是有失优雅,但是对于本蒟蒻来说已经很不容易了,希望以后能抽出时间对多线程进行深入学习吧。保持敬畏,谦而不卑,继续努力。

0114119),里面记录了我整体的思想变化和关键问题,不排除有错误的内容,请批判性的阅读,对于其中的大多数情况我都是自己打一个小的 demo 进行自己测试,我认为这样更有利于对正确性的验证和对整体的把握。

此外,一定要确保自己完全理解了多线程之间的相互关系再进行建模,否则就很有可能进入无限的 debug 中,笔者就与 CPU 超时斗智斗勇了整整三周星期…

“天高地迥,觉宇宙之无穷”,这一单元勉强算是笔者对于多线程的初探,提起多线程我总是想起我同学说的:“如果你认为自己已经学会了多线程,那么说明你还没有真正学会多线程”,将电梯建模为状态机,最后搓出来将近 200 行的 run 函数属实是有失优雅,但是对于本蒟蒻来说已经很不容易了,希望以后能抽出时间对多线程进行深入学习吧。保持敬畏,谦而不卑,继续努力。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值