文章目录
架构设计
调度器设计(没有调度器)
本单元作业我只有在hw_5中设置了调度器,并只有简单的按电梯id分配请求,故不再赘述
hw_6,hw_7均采用自由竞争的策略,即不实施调度,各电梯从总队列中自由获取请求
hw_5
第一次作业要求较为简单,故每个电梯都各自有一个候乘表,仅需要考虑这个表里的请求如何处理即可,所以重点关注电梯的运行策略。
以下是UML类图和协作图


-
Look策略
Elevator.javawhile (true) { end();//判断是否结束 //passengers.isEmpty() && allListEle.isEnd() && allListEle.isEmpty() strategy.reInitial(this);//为策略类更新电梯状态 end(); ups = strategy.getUps();//策略类给出要上电梯的请求 downs = strategy.getDowns();//策略类给出要下电梯的请求 direction = strategy.getDirection();//策略类给出电梯接下来的方向 boolean e = false; if (ifOpen(ups,downs)) {//判断是否开门 open();//开门 off(downs);//下电梯 on(ups);//上电梯 strategy.reInitial(this);//再次更新状态(判断此时是否可能转向) direction = strategy.getDirection(); ups = strategy.getUps(); on(ups);//新请求||因转向而可捎带的请求,上电梯 close();//关门 } end(); strategy.reInitial(this); direction = strategy.getDirection(); end(); move();//移动一层 }- 我采取的是将策略类对象作为电梯的一个属性,通过与电梯在特定时间交换信息实施决策,并指挥电梯的动作。特定时间即
strategy.reInitial(this)。将电梯的动作和对当前状态的分析决策分开。电梯不用关心目前有什么请求,只需要根据策略执行固定的动作。策略类也只需要把电梯传进来的信息做分析并返回决策即可。 - 在电梯开门且上下乘客后更新电梯状态,这样可以适应,电梯内最后一个乘客下电梯后,运行方向上只有本层有反向的请求,这样在开门的时候转向,并再一次上电梯,可以避免多次开门,或者反复折返而无法接到乘客的问题。
Srategy.javapublic void reInitial(Elevator elevator) { this.currentFloor = elevator.getCurrentFloor(); this.waitingListEle = elevator.getWaitingListEle(); this.allListEle = elevator.getAllListEle(); this.passengers = elevator.getPassengers(); this.direction = elevator.getDirection(); ups.clear(); downs.clear(); setWait();//判断电梯是否需要等待,若需要则直接在这里等待 setDirection();//判断是否转向 setDowns();//决定下电梯的人 setUps();//决定上电梯的人 } - 我采取的是将策略类对象作为电梯的一个属性,通过与电梯在特定时间交换信息实施决策,并指挥电梯的动作。特定时间即
hw_6
这次作业不再限制请求要乘坐的电梯,并加入了
Reset请求。
从这次作业开始,我实行自由竞争策略,故移除了Schedule,没有新增类。这次作业是改动最大的一次,相当于重构了。
以下是UML类图和协作图


- 加入了多套锁进行维护,锁系统我将在下面
同步块的设置和锁的选择标题中叙述 - 深克隆6个
allList队列,队列中包含所有类型的所有(包括其他电梯的Reset)请求,这几个队列将保证时刻同步- 对
allList的所有操作,都遍历对所有6个执行 - 为什么不用同一个:如果用同一个,新请求唤醒电梯时将只唤醒随机一个电梯,背离了自由竞争的观念,故一个电梯一个,唤醒时逐个全部唤醒
Input中无条件加入allList,只有乘客上电梯时或Reset执行完毕前从中移除相应请求,在Reset赶出未完成的请求时,重新加入6个allListallList参与构成每个电梯的结束条件,即当allList空时,可以确定所有请求处理完成(上了电梯的也不会下电梯),因为Reset请求在处理完毕前才删除,若有请求被赶出,应先加入allList,保持了allList的不空状态,不会提前结束
- 对
- 每个电梯有自己的
resetList队列Input中来Reset请求时,应(通过锁)保证同步加入allList和相应的resetList。避免被allList唤醒却发现没有Reset请求的错误- 移除也同步移除
- 电梯会定期监视,
resetList不空时,电梯尽早重置
- 将各电梯的每层请求队列
floorList.get(i)合并,变为总的floorList
Reset请求的处理
- 我将
Reset请求和Person请求视为同等请求均放入allList中 - 将
Reset的处理视为电梯的动作之一multiple.readLock().lock();//////////////multiple if (!resetList.isEmpty()) { multiple.readLock().unlock();////////////////multiple reset();//执行重置函数 continue;//相当于回到初始状态 } multiple.readLock().unlock();//////////////multiple reset()具体实现如果电梯不空,“赶出”乘客;//加回allList "RESET_BEGIN"; 将已`Receive`的请求释放掉; 移除Reset请求; sleep(1200); 置电梯属性; "RESET_END";- 每当乘客被赶出时,都要执行以下代码以更新请求
passengers.get(i).setFromFloor(currentFloor); passengers.get(i).setDispatched(0); passengers.get(i).setPrinted(false);
自由竞争的实现
由于本次课程组的限制,要求打出
RECEIVE以鼓励实现调度器,避免陪跑,故实现自由竞争较为困难,没有RECEIVE的电梯不能移动,被RECEIVE的乘客必须乘坐指定电梯。
- 但是,考虑到有乘客或打出过
RECEIVE的电梯可以自由移动Person请求类添加isDispatched和isPrinted属性,分别标志分配给哪个电梯(0代表未分配,k代表分给id为k-1的电梯) 和是否打印RECEIVE- 当乘客不空时,正常竞争运行,过程中捎带的乘客在上电梯的同时根据
isPrinted决定是否打印RECEIVE - 乘客空时,按hw_5思路,判断是否
wait,只需在wait的条件中加入一个新条件judge()while (judge() && !(allList.get(elevatorId - 1).isEnd() && allList.get(elevatorId - 1).isEmpty())) { allList.get(elevatorId - 1).wait(); }
judge()的思路public boolean judge() { 遍历allList.get(elevatorId - 1){ 遇到乘客请求(isDispatched()==elevatorId + 1){ 根据isPrinted()是否打印: return false;//不等待 } 遇到自己的Reset请求{ return false;//不等待 } } 再次遍历allList.get(elevatorId - 1){ 遇到乘客请求(isDispatched()==0){ setDispatched(elevatorId + 1);//抢请求 打印RECEIVE; setPrinted(true);//标记已打印 return false;//不等待 } } return true;//最后得出要等待 }- 这样,乘客空或刚被唤醒的电梯,无法移动,会去总队列中“抢”一个请求并打出
RECEIVE,获得移动权,只要运行起来,接下来就可以实现自由竞争了
hw_7
加入第二类重置请求以及双轿厢。架构几乎没有变化,为策略和电梯实现了接口。
以下是UML类图和协作图


还有对各种请求的管理

DcReset请求的处理
- 与之前的
NormalReset一样将DcReset请求和其他请求视为同等请求均放入allList中 - 与
NormalReset共同放入resetList DcReset的具体处理与NormalReset相同,视为电梯的动作之一multiple.readLock().lock();//////////////multiple if (!resetList.isEmpty()) { multiple.readLock().unlock();////////////////multiple if (choose()) { resetNormal(); } else { resetDc(); return;//关闭电梯 } continue; } multiple.readLock().unlock();////////////////multipleresetDc()具体实现如果电梯不空,“赶出”乘客;//加回allList "RESET_BEGIN"; 将已`Receive`的请求释放掉; 移除Reset请求; sleep(1200); 置电梯属性; "RESET_END"; this.elevatorA.start();//启动A轿厢 this.elevatorB.start();//启动B轿厢
双轿厢电梯的实现
- 实现
Elevator接口,下接NormalElevator和DcElevator两种电梯 - 实现
Strategy接口,下接NormalStrategy、DcStrategyA和DcStrategyB三种策略,本质都是look策略,细节处理有所差异。这样不用修改过多代码即可实现双轿厢 - 在
Main中创建18个电梯线程(6个normal,12个Dc),并创建好各自的策略,传入对应电梯,随即启动6个Normal电梯,其余12个等待启动 allList也变为18个,启动后的A、B轿厢和其他单轿厢电梯地位相同,共同竞争DcElevator在每到达一层开门下完人后,判断是否为换乘楼层,若是且有乘客则赶人。注意先加入allList再清空乘客容器,防止两队列全空的间隙,另一个轿厢提前结束(见下一条)- 由于
DcElevator在换乘楼层赶人增添新请求,影响到其他电梯的结束,故我让其另一个轿厢监管这个请求,防止请求被遗弃,即将DcElevator的结束条件新增一项fellow.getPassengers().isEmpty()//另一个轿厢乘客空 - 然后就是一些对
DcStrategy细节上的修改,不赘述,如何防撞在下面专题介绍
同步块的设置和锁的选择
由于后两次作业采用了自由竞争的策略,故线程安全较难保证,于是我设置了较为复杂的锁系统,自认为还算安全,但由于锁太多,等待锁的时间较长,所以最后的性能分也不算很高,但也很不错了。
| 锁 | 类型 | 作用 |
|---|---|---|
| multiple | ReentrantReadWriteLock | 保证18个allList,floorList以及resetList的同步修改 |
| allList.get(i) | 对象锁 | 保证在遍历某个allList时,其不能被改变 |
| rwLock | ReentrantReadWriteLock | 共有11个,维护11个每层的请求队列floorList.get(i) |
| personLock | ReentrantLock | 维护Person请求中isPrinted,isDispatched属性 |
| DcLock | ReentrantLock | 防止轿厢碰撞 |
- 锁的嵌套关系 (由外到内):
multiple > allList.get(i) > personLock multiple > rwLock > personLock allList.get(i) > DcLock- 严格按照这样的嵌套关系可以避免死锁
- 读写锁可以省去读-读等待锁的时间
personLock是电梯自由竞争的关键,能避免多个电梯同时抢到同一乘客,优先抢到锁的电梯才能修改请求的isDispatched属性为自己的标志- 下面举两个我代码中的例子
Input.javaif (request instanceof NormalResetRequest) {//读入新的Reset请求 Reset reset = new NormalReset(*); multiple.writeLock().lock();//保证18个`allList`和`resetList`的同步修改 for (int i = 0; i < 18; i++) { allList.get(i).addRequest(reset); } resetList.get(((NormalResetRequest) request).getElevatorId() - 1).addRequest(reset); multiple.writeLock().unlock();NormalElevator.javapublic void on(ArrayList<Person> ups) {//上电梯 if (!ups.isEmpty()) { for (int i = 0; i < ups.size() && passengers.size() < capacity; i++) { if (!ups.get(i).getPrinted()) { TimableOutput.println(String. format("RECEIVE-%d-%d", ups.get(i).getPersonId(), id)); } TimableOutput.println(String. format("IN-%d-%d-%d", ups.get(i).getPersonId(), currentFloor, id)); passengers.add(ups.get(i));//不共享,不用维护 multiple.writeLock().lock();//保证18个`allList`和`floorList`的同步修改 writeLockListOn.get(currentFloor - 1).lock();//维护floorList.get(currentFloor - 1) for (int k = 0; k < 18; k++) { allList.get(k).removeRequest(ups.get(i)); } floorList.get(currentFloor - 1).removeRequest(ups.get(i)); writeLockListOn.get(currentFloor - 1).unlock();//释放 multiple.writeLock().unlock();//释放 } } }
check-then-act
本单元作业中涉及多处check-then-act线程安全隐患,研讨课上有同学分享并没有维护也没有出问题,的确这种问题出现bug的几率极小,但我认为也是必须要维护的
- 例如:电梯等待的条件
synchronized (allList.get(elevatorId - 1)) { while (judge() && !(allList.get(elevatorId - 1).isEnd() && allList.get(elevatorId - 1).isEmpty())) { allList.get(elevatorId - 1).wait(); } } //////////////比较 while (judge() && !(allList.get(elevatorId - 1).isEnd() && allList.get(elevatorId - 1).isEmpty())) { synchronized (allList.get(elevatorId - 1)) { allList.get(elevatorId - 1).wait(); } }- 上述两种写法,显然上面的是线程安全的,下面的代码刚进行完
while判断,判断为真后,很可能其值被修改为假,然而此时已不可避免的执行wait(),导致最后无法被唤醒;还有可能在多条件判断时,前面条件判断时,后面的条件可能会被修改,造成误判。 - 但是事实上,上面的写法运行时间会长于下面,因为临界区域大,但这是必要的,所以我认为本单元作业,最重要的不是去卷性能,意义不大,最重要的是更深刻的理解和掌握线程安全,这对未来的帮助很大。
- 上述两种写法,显然上面的是线程安全的,下面的代码刚进行完
轿厢碰撞问题
我的设计是:
- 每当
Dc电梯即将前往换乘楼层时,询问另一轿厢是否正处在换乘楼层,是则等待,否则正常运行 - 每当
Dc电梯正常移动前,判断是否处在换乘楼层,是则唤醒另一轿厢(无论其是否在wait中) - 每当
Dc电梯因缺乏请求而即将wait()前,先强行将其移离换乘楼层,随后唤醒另一轿厢,再wait(),防止占用换乘楼层进行等待
具体实现则用到了上面提到的DcLock
DcElevator.java - move()
//例,此时若方向向上
synchronized (lock) {//DcLock
if (type == 'A' && currentFloor == transferFloor - 1 && fellow.getCurrentFloor() == transferFloor) {
try {
lock.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
sleep(*);
TimableOutput.println(String.format("ARRIVE-%d-%d-%c", currentFloor + 1, (id - 5) / 2, type));
currentFloor++;
if (type == 'B' && currentFloor == transferFloor + 1) {
lock.notifyAll();
}
}
- 两个
if都要在锁中,保证一个轿厢移动完,另一个轿厢才开始判断,先判断再阻塞没有意义 - 一定将
currentFloor++;放在锁内,保证释放锁的时候,楼层已修改,否则另一轿厢得到的信息会延迟
bug分析
三次作业只有第二次强测出现了bug,未RECEIVE就移动,可以确定是线程安全问题引发的WA,但经多次检查,并没有发现哪里不安全,但定位到了具体出问题的代码,于是加了一个特判,补上了RECEIVE,后续也没有新的bug,但还是没从根源解决问题,一直很苦恼。
- debug方法
- 如果是WA,就比较好debug,根据代码逻辑到对应位置检查就好了(除了线程安全bug)
- 如果是CTLE,基本可以确定
wait()失效,导致轮询 - 如果是RTLE,就是死锁,或者
wait()无法被唤醒
心得体会
最大的体会就是,理解和掌握线程安全和各种锁的使用非常重要。虽然没有在调度器以及调度策略上费工夫(卷性能),但在设计各种锁,维护我的自由竞争的一大坨线程安全问题的时候,也绞尽脑汁,在这个过程中,我对锁的理解有了进一步的提升。
本文详细描述了作者在编程作业中设计电梯调度系统的过程,包括没有调度器时的自由竞争策略,如何处理Reset和DcReset请求,同步块的设置,锁的选择,以及解决轿厢碰撞问题和线程安全挑战的心得体会。作者强调了理解线程安全和正确使用锁在设计中的重要性。
6万+

被折叠的 条评论
为什么被折叠?



