2021年春-OO-第二单元总结
2020春-OO-第二单元总结
1. 第一次作业
第一次作业模拟了单个电梯的运行。
我使用了生产消费者模式,有四个线程,一个是主线程,另一个是input线程(生产者),另一个是Scheduler调度器线程(托盘),最后一个是process线程(消费者)。每次从生产者获取请求,然后使用调度器来调度请求,最后使用消费者线程,来处理调度器分配的请求。
同步块设置:
我有三个同步块:
- 在消费者线程里,有几个同步块,其目的是防止在判断时,处理队列和等待队列被其他线程使用,导致判断错误。
synchronized (processingQueue) {
synchronized (waitQueue) {
if (processingQueue.isEmpty() && waitQueue.noWaiting() && waitQueue.isEnd()) {
//TimableOutput.println("结束了Process线程");
return;
}
}
try {
processingQueue.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
- 输入线程,其向等待队列投入请求,所以,在投入请求的时候,其他线程不应该操作该等待队列。
synchronized (waitQueue) {
if (personRequest == null) {
waitQueue.close();
waitQueue.notifyAll();
return;
} else {
Request request = new Request(
personRequest.getPersonId(),
personRequest.getFromFloor(),
personRequest.getToFloor()
);
waitQueue.addRequest(request);
waitQueue.notifyAll();
}
}
- 调度器线程,每次需要从等待队列取出请求,然后向处理队列投放请求,所以,在取出请求和投放请求时,我们都需要使用同步块,防止处理时不同步。
synchronized (waitQueue) {
if (waitQueue.isEnd() && waitQueue.noWaiting()) {
//TimableOutput.println("结束调度线程");
synchronized (processingQueue) {
processingQueue.notifyAll();
}
return;
}
if (waitQueue.noWaiting()) {
try {
waitQueue.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
temp.addAll(waitQueue.getRequests());
for (int i = 0; i < temp.size(); i++) {
Request request = temp.get(i);
synchronized (processingQueue) {
processingQueue.addRequest(request);
processingQueue.notifyAll();
}
temp.remove(request);
i--;
}
waitQueue.clearQueue();
}
}
调度器的设计:
如下代码所示,由于只有一个电梯,所以调度器使用的线性调度,每次仅仅从等待队列waitQueue来取出请求,然后遍历请求,逐个插入处理队列,但后唤醒notifyAll处理Process进程中的锁,让其处理请求。
temp.addAll(waitQueue.getRequests());
for (int i = 0; i < temp.size(); i++) {
Request request = temp.get(i);
synchronized (processingQueue) {
processingQueue.addRequest(request);
processingQueue.notifyAll();
}
temp.remove(request);
i--;
}
waitQueue.clearQueue();
三种方法:
-
Night模式:
night模式,我使用的方法是先上升到最高处,然后向下取人,直到取满,然后运送回到底部。
-
Morning模式:
我在Random的基础上,减少了从上至下取人的阶段,然后增加了等待时间,并判断时间间隔是否过大,当过大就直接开走。
-
Random模式:
电梯开始时会一直向上走,同时遇到人,不满时接取人,如果发现有需要离开的,就开电梯门送出乘客。这样一直向上走,直到电梯所在层之上的楼层没有乘客需要进入,并且电梯内的乘客不需要在上面楼层出去,就向下走。向下时方式同向上。
UML类图:

UML协作图:

可扩展性分析:
在第一次作业中,我尽力降低程序间的耦合程度,将每个类能独立的,都独立出来。这样使得我在后续工作中,减小了工作量,扩展性较高:
1.比如对于电梯类的处理部分,我在第一次作业就将各种参数独立为变量,构造时进行赋值,方便了许多,也对之后扩展为不同类型的电梯奠定了基础:
public Elevator(int minFloor, int maxFloor, int initFloor, double speed,
double openSpeed, double closeSpeed, int maxPerson,
ProcessingQueue processingQueue
) {
this.maxFloor = maxFloor;
this.minFloor = minFloor;
this.initFloor = initFloor;
this.speed = speed;
this.openSpeed = openSpeed;
this.closeSpeed = closeSpeed;
this.maxPerson = maxPerson;
this.nowFloor = initFloor;
this.processingQueue = processingQueue;
this.status = "up";
this.nowPerson = 0;
elevatorRequest = new ArrayList<>();
isOpen = false;
}
2. 第二、三次作业:
第二次作业要求增加了多部同型号的电梯,并且可以动态增加电梯,合适调度。
第三次作业增加了多部不同型号的电梯,可以增加电梯,合适调度。
由于我没有重构,第二、三次作业的结构同第一次。
UML图:

UML协作图:

调度器的设计:
我使用了综合调度的方式,调度仅存在于这个调度器中,由调度器分配一切等待请求。我使用了随机分配的方式。
{
temp.addAll(waitQueue.getRequests());
for (int i = 0; i < temp.size(); i++) {
MyRequest myRequest = temp.get(i);
int j = random.nextInt(processingQueueArrayList.size());
while (elevatorArrayList.get(j).getReach()[myRequest.getFromFloor()] == 0 ||
elevatorArrayList.get(j).getReach()[myRequest.getToFloor()] == 0) {
j = random.nextInt(processingQueueArrayList.size());
}
synchronized (processingQueueArrayList.get(j)) {
processingQueueArrayList.get(j).addRequest(myRequest);
processingQueueArrayList.get(j).notifyAll();
}
temp.remove(myRequest);
i--;
}
waitQueue.clearQueue();
}
可停靠楼层的方法:
由于前文的铺垫,我只需要在构造函数中更改了一些设置,就可以更改电梯的类型了。
public Elevator(ProcessingQueue processingQueue, String id, String type) {
this.id = id;
this.maxFloor = 20;
this.minFloor = 1;
this.initFloor = 1;
this.speed = 0.4;
this.openSpeed = 0.2;
this.closeSpeed = 0.2;
this.maxPerson = 6;
this.nowFloor = initFloor;
this.processingQueue = processingQueue;
this.status = "up";
this.nowPerson = 0;
elevatorMyRequest = new ArrayList<>();
isOpen = false;
for (int i = 1; i <= 20; i++) {
reach[i] = 0;
}
if (type.equals("A")) {
speed = 0.6;
maxPerson = 8;
for (int i = 1; i <= 20; i++) {
reach[i] = 1;
}
} else if (type.equals("B")) {
speed = 0.4;
maxPerson = 6;
for (int i = 1; i <= 20; i++) {
if (i % 2 == 1) {
reach[i] = 1;
}
}
} else if (type.equals("C")) {
speed = 0.2;
maxPerson = 4;
for (int i = 1; i <= 3; i++) {
reach[i] = 1;
}
for (int i = 18; i <= 20; i++) {
reach[i] = 1;
}
}
}
3. 分析程序的bug及Hack策略
本人bug:
- 死锁:在调度器中,我起初使用了 等待——唤醒——等待的方式来调度,发现提交上去会超时,判断产生了死锁,比如Night,所有请求会一同进入,这样在唤醒时,就将所有的请求放入了处理队列,那么第二个等待就会处于未唤醒状态,直到被评测机杀死。
//等待
if(waitQueue.noWaiting()){
try {
waitQueue.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//唤醒操作
temp.addAll(waitQueue.getRequests());
for (int i = 0; i < temp.size(); i++) {
Request request = temp.get(i);
synchronized (processingQueue) {
processingQueue.addRequest(request);
processingQueue.notifyAll();
}
temp.remove(request);
i--;
}
waitQueue.clearQueue();
//等待
if(waitQueue.noWaiting()){
try {
waitQueue.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
分析bug
-
列出自己所采取的测试策略及有效性
我使用了评测机+阅读代码的方法,我因为没有有效的定位方法,当评测机发现bug,我会查看该同学的问题,然后根据提供的输入,手动去阅读代码和调试。
-
分析自己采用了什么策略来发现线程安全相关的问题
主要是一些极限情况,比如有的测试点,直到70多秒才有了输入,很容易卡某个人的tle。
-
分析本单元的测试策略与第一单元测试策略的差异之处
第一单元的测试更加方便,并且拥有着一些可靠的库来帮助我们进行判断是否正确,并且答案相对固定,可以对拍,而现在答案不确定性太大,没有统一的方法。并且时间控制较为麻烦。
4. 心得体会
- 从线程安全和层次化设计两个方面来梳理自己在本单元三次作业中获得的心得体会
线程安全:
- 保证在使用共享数据时,使用synchronized块,不论是同步某个变量,还是使用它修饰某个方法,都可以保证该变量or方法在使用时,不会被其他线程调用,保证了线程安全。但同时,也要保证不能出现死锁,两个线程分别锁住了a,和b,但分别又去访问b和a,那么就会产生死锁,这样在自己设计时注重一下就可以避免。
- 避免轮询,使用wait,notify的方法更加便捷与安全,减少了cpu占用。
层次化设计:
层次和设计比较重要,因为我第一次作业层次化设计很好,在后来的作业中,我没有多大改动就能提交了。但我有很多的不足是,我没有使用太多的继承关系和抽象方法、接口,因为我觉得受限于checkstyle,父类无法使用protected修饰变量,导致子类访问父类的公共变量时极其麻烦,所以让我产生了畏惧。未来应当思考这些问题的解决方法。

浙公网安备 33010602011771号