BUAA-OO第二单元总结
1.单元介绍与整体认识
(1)单元介绍
本单元主要进行多线程程序的设计。基本目标是
①通过模拟多线程实时电梯系统,熟悉线程的创建和运行等基本操作,并熟悉多线程的设计方法;
②掌握线程安全的知识并解决线程安全问题,同时进行一定程度的线程协同;
③掌握进程之间的交互,强化线程之间的协同设计层次架构。
(2)整体认识
本单元通过使用Thread类和runnable接口、synchronized关键字,实现多线程的同步控制、线程安全、线程协作等功能。过程中,可以使用 “生产者-消费者”、“流水线”、“传送带” 等设计模式进行设计。
生产者-消费者模式
在本次设计中,我主要采用了 “生产者-消费者模式” 。将乘客请求看成“资源”,那么生产资源的类InputGetter
称为“生产者”,消耗资源的类elevator
称为“消费者”。生产者从输入中读入request并将其加入到托盘中,消费者线程则从托盘中取出资源并将之进行处理。这种设计模式实现了读入线程和电梯线程的协作。
(3)整体架构
作业思路简介
因为第三次作业是本单元三次任务的“集大成者”,因此我决定在这里先介绍一下整体的实现思路,方便后续内容的展开。
任务:实现自定义电梯和电梯之间的协同运作。
首要目标:实现“双跨”(跨楼层、跨楼座)请求的处理
方案:
实现了一个PersonRequest的包装类MyPersonRequest,其中有一个PersonRequest的arraylist,在读入每个请求后就将其拆分为1-3个同楼层或同楼座的基本请求(下面称为元请求),然后将这几个元请求按需要被执行的顺序进行排序,并按顺序插入arraylist中。在处理这个请求时,每个电梯只读取arraylist中的第一个元请求,并将乘客搭载到第一个元请求的目的地,然后判断该乘客是否已经到达最终的终点。如果没有,说明还需要继续换乘,就将arraylist中的第一个元请求remove掉,然后加入下一个元请求需要搭载的电梯中,以此类推。
这样就实现了对每个请求统一化的静态调度。
UML类图如下
模块功能介绍:
模块名称 | 模块功能 |
---|---|
InputGetter | 从标准输入中读取两类请求 |
Elevator、Lateral Elevator | 实现两种电梯的属性和行为 |
Controller | 实现增加电梯和初始化电梯等操作 |
(schedule) | 在前两次作业中作为调度器,分配二级托盘中的请求 |
RequestLists、RequestList | 对容器进行封装,作为托盘 |
MyPersonRequest | 在第三次作业中对PersonRequest进行封装,实现静态调度 |
(4)电梯设计思路
电梯内部实现的顶层代码如下:
public void run() {
while (true) {
dir = getDir(); //预测下一次运动的方向
running = getRunning(); //判断是否需要保持运行状态
if (dir == 2) {
break; //如果判断运行已经结束,dir = 2,电梯停止运行
}
if (hasIn() | hasOut()) {
open(); //如果有在本层上或下电梯的请求,则开门
}
move(); //根据先前判断的方向运行一层或停靠一次。
}
}
其中的核心为判断下次运行方式的函数getDir
public int getDir() {
//如果电梯中有人(本层之后还有人)
boolean stillHasPerson = false;
for (MyPersonRequest inRequest : inRequests) {
int toFloor = inRequest.getToFloor();
int fromFloor = inRequest.getFromFloor();
if ((toFloor - floor) * dir > 0) {
stillHasPerson = true;
return dir;//方向不变
}
}
synchronized (outRequests) {
if (!stillHasPerson) { //如果电梯中没有继续沿当前方向的乘客
int minDistance = 20;
int dir = 0;//没有请求
if (outRequests.isEnd() && amount == 0 && outRequests.isEmpty()) {
return 2;
}
if (outRequests.isEmpty()) {
return 0;
}
for (MyPersonRequest request : outRequests.get()) {
int fromFloor = request.getFromFloor();
int toFloor = request.getToFloor();
int distance = Math.abs(fromFloor - floor);
if (distance < minDistance) {
minDistance = distance;
if (fromFloor - floor > 0) {
dir = 1;
}
else if (fromFloor - floor < 0) {
dir = -1;
}
else { //如果请求就在这一层
dir = (toFloor > floor) ? 1 : -1;
}
}
}
return dir;
}
}
return dir;
}
(5)迭代情况
hw06迭代设计
第二周作业迭代需求
-
增加了横向电梯
-
增加了增加电梯的功能,可以增加横向和纵向的电梯
-
隐式增加了一个功能,就是多个电梯间的请求如何分配的问题。
第二周修改条目
针对横向电梯
-
需要增加一个电梯类,模仿纵向电梯进行设计(将电梯类变成父类,下面有纵向和横向两个子类)
-
需要增加一个横向请求的托盘,在input的时候分类加入。
-
需要在input类中分类处理两种请求:乘客和增加电梯,如果是增加电梯就直接在调度器类中使用addElevator方法;如果是乘客请求,就分横向和纵向放到两个托盘中。
hw07迭代设计
功能增加
-
乘客目的和出发点不在同一层、同一座
-
电梯定制化
-
纵向电梯:
-
运行速度
-
容纳人数
-
-
横向电梯:
-
运行速度
-
容纳人数
-
可开关门信息
-
-
设计更改
-
更改电梯的默认速度和载荷 x
-
增加在A座1楼的初始横向电梯(6号) x
-
横向电梯和纵向电梯进行修改,符合本次的设计和定制化要求 x
-
修改增加电梯的方法,(实现自定义电梯) x
-
在电梯类中进行修改,使得在乘客出电梯的时候进行判断,是否真的到达终点.
-
修改schedule,判断需要中转的请求.对于需要中转的请求,增加back函数和每个请求对应数组中的一个位置,这里存储中间目的地,比如从A1到B2,且三楼有从A到B的横向电梯,则先将目的地设置为A3,并将B2存到数组中;到达A3之后,不remove而是将数组中的B取出并赋给请求,然后将请求重新加入waitList,等到达B3后,最后将数组中的2取出并赋给request,将请求重新加入waitList.(这个过程中有两个位置需要判断,一个是第一步,判断是否需要先纵向在横向,还是直接横向即可,第二个是在下了横向电梯之后,判断是否需要纵向电梯,还是已经到达目的地了.)
架构概览
V类(行为类):
-
Controller:用于管理电梯,负责电梯的初始化和增加电梯(主要对两种电梯的list进行添加操作)
-
InputGetter:读入请求并进行分类(空请求\增加电梯\乘客请求),交给相应的托盘(主要对wailLIst进行操作)
-
schedule:用于分配请求给电梯(从总的请求托盘放到各个层或座的托盘)
N类(对象类):
-
两个电梯类
-
RequestList:托盘类,进行了线程保护并封装了一个request的ArrayList.具体实现:
-
waitList:总托盘,在分配给各个电梯之前的乘客请求放在这里(但是本次作业应该重新考虑存在的意义了.因为可能影响了性能)
-
processingLists:一个requestList类的arraylist,存放了各个楼层和楼座的托盘
-
难点分析
在我看来最主要的难点在于如何通过或不通过中转的方法将乘客送到目标楼座.
有几种可能性,首先需要换乘的一定是起点和终点不在一座的;那么其中就有两种情况,起点和终点在同一层和起点终点在不同层.如果起点和终点在同一层,就直接由schedule判断是否可以直达,如果不能就用下面的方法;如果起点和终点不在同一层,也直接用下面的方法.
到这里应该都是需要换乘的请求了,既然需要换乘,就统一使用下面的算法.
应该在schedule处对这种需要中转的request进行加工:先对所有横向电梯进行判断,看哪个电梯可以实现中转;在所有可以实现中转的电梯中选择处于起始楼层和终止楼层之间的,处于中间的电梯中优先选择在起始楼层的和终止楼层的.确定了让谁来转运之后
针对多电梯分配
-
听说谁抢到算谁的,先试试这种方法,不行的话就换成轮流给。
2.同步块设置和锁的选择
我的设计主要采用了托盘化的设计方法来实现多线程写作和保证线程安全。给托盘添加包装类,将其中用于对托盘元素进行读写的方法用synchronized
修饰,保证在同一时刻只有一个方法能够对托盘进行访问和修改。
同时,为了防止多个线程同时调用println导致的输出线程不安全,我采用了为println添加包装类的方法。
托盘类:RequestList
// 为托盘添加一个请求
public synchronized void addRequest(MyPersonRequest request) {
requests.add(request);
notifyAll();
}
// 从托盘中取出一个请求
public synchronized MyPersonRequest getOneRequest() {
if (requests.size() == 0 && !isEnd) { //如果为空且没有结束就等待
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
if (requests.isEmpty()) { //如果已经结束了
return null;
}
MyPersonRequest request = requests.get(0);
requests.remove(0);
notifyAll();
return request;
}
//直接返回托盘中的容器本身
public synchronized ArrayList<MyPersonRequest> get() {
while (requests.size() == 0 && !isEnd) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
return requests;
}
//从托盘中删除一个元素
public synchronized void remove(MyPersonRequest request) {
requests.remove(request);
notifyAll();
}
//返回托盘是否已经结束
public synchronized boolean isEnd() {
return isEnd;
}
//将托盘状态设置为结束
public synchronized void setEnd() {
this.isEnd = true;
notifyAll();
}
//判断托盘当前是否为空
public synchronized boolean isEmpty() {
notifyAll();
return requests.isEmpty();
}
输出包装类:MyOutput
//使用同步方法包装println方法,实现输出线程安全
public static synchronized long println(String str) {
return TimableOutput.println(str);
}
3.调度器设计
在本次作业中,我尝试了两种调度器策略:无调度器-单级托盘架构
和 有调度器-两级托盘架构
(1)有调度器-两级托盘
在本单元第一次和第二次作业中,我采用了调度器+两级托盘的调度方式,其中设计了一个总托盘用于接收inputGetter读入的所有personRequest请求,再通过每层和每座的调度器schedule
共同从总托盘中取出属于自己层或座的请求并放到二级托盘中,再由电梯从二级托盘中读取并处理这些请求。
(2)无调度器-一级托盘
本单元第三次作业中,为了方便电梯线程之间的协作,我进行了一定程度的重构,删除了总托盘和调度器结构,采用自由竞争策略,即inputGetter在读入请求的同时直接完成请求的静态规划并分配到相应的层或座托盘中。电梯在处理完请求后,判断请求是否已经达到最终目的地、还是只完成了某一段中转,如果是后者,就将之加入下一个电梯的托盘中。
4.架构设计
(1)整体思路
请见第一部分
(2)UML类图和UML协作
UML类图:
UML协作图:
电梯线程:
主线程时序图:
inputgetter时序图:
5.bug分析和hack策略
bug分析
三次作业中出现了两个bug,都出现在第三次作业的强测,是在考虑轮询情况的时候有缺漏所导致的CTLE,在课下自己测试的时候脑子有点懒没有考虑周全,自作自受。
在自测中发现的bug主要还有因为判断条件而导致的换乘静态规划错误(直通地心)和线程终止条件错误等问题。
第一次作业还因为没有考虑输出线程安全的问题而在互测中被查出bug,之后在bug修复中通过使用包装类将println方法用synchronized关键字修饰的方式解决了线程不安全的问题。
hack策略
虽然本人没有对别人的代码进行hack,但是我认为可以通过分析和借鉴往次强测数据的构造方法来有针对性地构造数据。强测数据一般都比较有针对性,每组数据针对一个或几个可能的出错点进行检查;因此在构造互测数据的时候也可以学习借鉴其hack思路。
同时,有精力的情况下也可以使用评测机进行评测。
6.心得体会
(1)线程安全
线程安全主要通过线程安全类
的设计来保证。一般通过对共享数据类进行包装,并使用synchronized关键字和wait、notifyAll方法来保证线程之间对共享数据操作的互斥性,进而达到线程安全的目的。
同时,在加锁时也不能盲目,否则会导致多线程高效的优势不在明显、效率明显降低。
(2)层次化设计
本次作业的层次化设计主要体现在行为的层次化上。通过不同层次的类的设计,实现从高层次的操作(如电梯增删)到底层操作(如电梯在层间和座间的原子级运行)。
另一个层次化的体现是迭代中的层次化。第一次任务实现了每个电梯的行为控制,第二次任务实现了多电梯协作处理一个托盘中的内容,第三次任务实现了对每个请求进行静态调度、不同楼层楼座间的电梯协作。