301
社区成员
发帖
与我相关
我的任务
分享三次作业中同步块的设置和锁的选择(见0.2)
调度器设计
调度策略优化
实现双轿厢的两个轿厢不碰撞(见3.2)
bug与debug(见4,5)
心得体会(见6)
本单元主要考查了多线程和锁。
多线程方面,主要涉及开启:.start(),运行:.run(),结束:.run()中跳出while(true)循环(break/return)。这一点看课程组给的demo样例很容易能学会。
锁方面,其实前两次作业我对锁的概念和使用都囫囵吞枣,直到hw6强测wa了两个点。我主要使用了synchronized这种锁,它有以下三种使用方式:
修饰静态方法
修饰实例方法
修饰代码块
由于不同线程之间,只需要对共享对象的写入或移除上锁,所以我采用了修饰代码块这种方式,以下是一个例子。
synchronized (globalRequest) {
while (globalRequest.getSize() > 0) {
processPerson(globalRequest.getRequest(0));
globalRequest.removeRequest(globalRequest.getRequest(0));
}
globalRequest.notifyAll();
}
UML图

线程关系

架构分析
Main类:创建含六部电梯的Arraylist<Elevator> elevators,开启Scheduler和六个Elevator进程。
RequestPool类:请求的列表集合。
Scheduler类:主要承担了读取请求、向对应电梯加入请求的功能,由于本次作业指定了每个请求对应的电梯、不需要自己写分配策略,所以结合后续作业来看,这里的Scheduler命名为Input更合理一些。
Elevator类:主要承担了电梯的运行。请求存放方面,每部电梯创建了inRequest和outRequest两个请求池来存放该部电梯电梯里外的请求,每当一个请求进入电梯,则由oueRequest转移至inRequest;电梯运行方面,主要借鉴了BUAA-OO-第二单元:电梯调度 | YannaのBlog (gitee.io)的思路,以下是一个简略的思维导图:

代码量

类复杂度

Elevator类中包括了大量的运行判断:
searchMain(),open()run()judgeOpen()open2(),open4()open1()而无论是寻找主要请求、是否开门,还是进人、出人,几乎都要遍历inRequest和outRequest,因此复杂度较高。
优点
outRequest的添加和移除上锁,上锁严谨且完全,没有出现死锁、上锁错误、缺锁导致的bug。缺点
Scheduler类承担了读取请求、分配请求两重功能,结构不清晰,不利于后续迭代。UML图

线程关系

将上次作业中的Scheduler类解耦成Input类和Scheduler类:
Input类:
Scheduler类:负责从 globalRequest 读取请求并分配,通知对应电梯处理请求。
分配策略方面我选择了 满足捎带>出发地再电梯运行方向上(优先最近)>电梯现有请求数最少 三种分配方式,永远存在电梯请求数最少的电梯,故请求总能被分配。
Elevator类中新增了对reset的处理:
arrive,开始reset处理。代码量

类复杂度

Elevator类
cleanIn(),cleanOut():需要遍历inRequest,outRequest,将请求移至 globalRequest,复杂度较高Scheduler类主要包括四种分配策略(way3()弃用):
way1()way2()way4()每种分配都需要遍历各部电梯、获取每部电梯的详细信息,复杂度较高。
优点
若电梯内请求不为空,则最多输出两个arrive,开始reset处理:这种方式能使电梯内的请求离toFloor更进一步,优化性能。(考虑过比较 该请求目前所在的电梯的移动速度 和 重新分配后所在的电梯的移动速度,但由于清明节想出去玩没有实现)
起初我没有给正在reset的电梯分配请求,在舍友的帮助下,注意到 “全部电梯一起reset、reset_end之后所有请求都分配给第一部reset_end的电梯” 的情况,于是想到了暂存的方法(reset过程中仍可向该电梯加入请求,这些请求被暂存在requestTemp中,在reset结束后被移入outRequest)。
(同样是在舍友的帮助下)我学习了量子电梯,虽然在时间上与更合理的分配相比微乎其微,但扩展了思路。
openPoint = System.currentTimeMillis();
// 开门操作(进人、出人)
closePoint = System.currentTimeMillis();
try {
sleep(max(openTime + closeTime - (closePoint - openPoint), 0));
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
缺点
Scheduler类从 globalRequest 读取请求后,删掉请求时,没有对其上锁,导致强测互测中出现了如下的bug:
UML图

线程关系

Input类同上。
Scheduler类:分配策略基本同上。只添加了:出发层在电梯运行范围内。(这里出现了一个巨大的bug,导致部分情况性能很差)
将上次作业中的Elevator类分为三个类:
Elevator类:新增了type(表示电梯类型:null/A/B),transferFloor(换乘楼层),lowFloor/highFloor(电梯的最低层和最高层),使得两类电梯使用同一套运行逻辑;新增requestPoolA和requestPoolB,使得电梯在进行DoubleReset时,也能从外界读入请求,分别暂存在以上两个容器中,并在A/B电梯线程新建时,读入到对应电梯中的outRequest中。
Operation类:将电梯运行中冗杂的判断开门/寻找主要请求操作挪到了该类中。
Input类:输出语句类
代码量

类复杂度

Operation类包含了电梯运行过程中大量冗杂的判断函数(open(),searchMain()),复杂度较高。
Scheduler类主要包括三种分配策略way()以及对应的优先到达层在输出范围内的分配策略mainWay(),每种分配都需要遍历各部电梯、获取每部电梯的详细信息,复杂度较高。
Elevator类中,当对应的“朋友电梯”在换乘层时,该电梯不能达到换乘层,因此在up()和down()方法中包含了本电梯的wait和对方电梯的notifyall:(实现双轿厢的两个轿厢不碰撞)
public void Up() {
int lastFloor = nowFloor;
if 不是换乘电梯:上行(nowFloor++)
else if 是A电梯:
if 将要行至换乘层:
if 换乘层未被占用且暂时不会被占用:上行(nowFloor++)
else transferFloor.wait();
else:上行
else:上行(nowFloor++)
if (是换乘电梯 且 lastFloor是换乘层) { // 该电梯从换乘层离开
synchronized (friend.transferFloor) {
friend.transferFloor.notifyAll();
}
}
}
优点
缺点
openA(),openB(),其实与之前原有的判断条件有重合,但我没有做详细梳理,而是整个一坨加在了原先开门条件的前面,代码逻辑冗杂。Elevator类远远超出了500行限制,不得不新建一个Operation类、将判断操作挪出来。在hw6强测和互测中,暴露出一个bug,即上锁不完全——Scheduler类从 globalRequest 读取请求后,删掉请求时,没有对其上锁。导致某些时候分配请求错乱、输出错乱。
在hw7强测中,部分电梯分裂的情况,请求会涌入正常的电梯,运行时间过长,产生TLE。(写的时候脑子不清楚写反了,考虑到耗电,应该优先考虑双轿电梯,而且我实际的实现是先考虑正常电梯,并且由于有一个寻找最少请求的方法,导致只有一部正常电梯时所有请求都涌入该部电梯)。
在交作业前、自己测试中那可就出现了不少问题
在写博客的时候发现了hw7的一个bug,在分配策略中,笔误把一个getReset()写成了getDirection(),还好问题不大,,因为最后请求总能分出去的……
arrive输出语句注释掉(arrive过多不易观察请求的进出)。当然记得提交的时候把它放出来:)同步方法和代码块:
在 Up() 和 Down() 方法中使用了 synchronized 块来同步访问 transferLock 对象。
Elevator和Input、Scheduler之间使用 synchronized 块来同步访问 outRequest 、 globalRequest 、 elevators 对象,因为这些对象涉及到多线程并发访问共享,因此上锁以确保线程安全。
线程间通信: 使用了 wait() 和 notifyAll() 方法来进行线程间的等待和唤醒,二者配对使用,避免死锁。
在换乘电梯中,一个电梯到达换乘层时,需要等待另一个电梯的信号;
在向电梯的 outRequest写入时,要及时唤醒电梯调度线程;
在向 globalRequest写入时,要及时唤醒分配调度线程;
在电梯reset后,要及时唤醒分配调度线程,判断是否可以结束进程;
共享资源保护: 使用了同步块来保护共享资源,如 outRequest 、 globalRequest 、 elevators 等。这些对象在多个线程中被访问和修改,因此需要进行合适的同步以保证线程安全。
Main、Input、Scheduler、Elevator四个线程,分别承担开启线程、处理输入、分配调度、电梯运行。Elevator 类中的方法根据功能被合理地划分,比如 Up()、Down()、Arrive()、Open()、Close() 等方法各自负责电梯的不同行为。这种划分使得代码逻辑清晰,易于理解。Elevator类拆分为Elevator类、Operation类、Input类,将接收信号/运行、操作、输出分离,使结构清晰。synchronized这一种锁,也只使用了修饰代码块一项功能,以后还是要继续探索。