OO Unit2 Summary

OO Unit2 Summary

目录

  • 一、三次作业分析

  • 二、bug分析

  • 三、心得体会

在第一部分将结合三次作业的uml类图,先分别介绍每次作业的设计思路、同步块与锁的选择以及调度器设计,再展示UML协作图。在第二部分将介绍程序bug、hack和test策略。第三部分讲述心得体会。

一、三次作业分析

1. 第五次作业

UML类图

设计思路

第五次作业要求实现5个单座电梯的运行模拟。

由于第五次作业仅仅为每座1个电梯,且没有动态增加电梯的需求,较为简单。因此第五次作业我采用了输入线程和电梯线程的双线程设计。以RequestTable类对象processQueue作为共享对象,实现输入线程与电梯线程之间的交互。对于电梯的运行,我通过strategy维护电梯的directiondestination变量,当电梯的positiondestination相同时,就执行开关门操作。而strategy采取的更新方式是look策略。

同步块与锁的设计

第五次作业在我的设计中,有下列情况需要加锁:

  • 共享对象方法加锁:在共享对象类内定义的方法中,如果涉及对其的读写操作,都需要加锁。

// RequestTable.java
public synchronized void addRequest(PersonRequest pr) {
   requests.add(pr);
   notifyAll();
}
  • 线程中加锁同步块:一些方法不便于直接定义在共享对象类中,但涉及对共享对象的读写操作,这时需要将其加锁。

// Elevator.java
public void pickUpPerson() {
   ArrayList<PersonRequest> renew = new ArrayList<>();
   synchronized (processQueue) {
       ArrayList<PersonRequest> requestTable = processQueue.getRequestTable();
       for (PersonRequest pr : requestTable) {
           if (pr.getFromFloor() == position && insidePerson.size() < capacity &&
                   strategy.toPick(position, direction, insidePerson.size(), pr,
                           processQueue)) {
               insidePerson.add(pr);
               SafeOutput.println(
                       String.format("IN-%d-%c-%d-%d", pr.getPersonId(),
                               id + 'A' - 1, position, id));
          } else {
               renew.add(pr);
          }
      }
       requestTable.clear();
       requestTable.addAll(renew);
  }
}

调度器设计

第五次作业我没有设计调度器线程,我采用的方法是直接在输入线程将对应请求分配到电梯的等待队列。电梯作为线程运行,strategy类帮电梯进行决策,策略是look。

2. 第六次作业

UML类图

 

设计思路

第六次作业增加了动态增加电梯和环形运输的需求。

对于动态增加电梯需求,我设计了调度器线程,用来响应增加电梯请求以及分派乘客。

对于环形运输需求,可以考虑把A->B->C->D->E->A定义为up方向,反向定义为down方法。我设计了一个方法类Direction,该类可以通过输入fromfloortofloorfromBuildingtoBuilding返回一个方向。由于环形电梯和线性电梯逻辑吻合度高,我给电梯增加了type属性,大部分方法得到了复用。只需要针对性设计动作act即可。

此外,由于第五次作业Elevator线程个人设计欠佳,维护directiondestination的方法过于臃肿,导致bug不断。所以第六次作业索性进行了重构,电梯的运行逻辑改为了有限状态机。

 

电梯的运行流程变成了状态转换state transfer以及根据状态运行act

同步块与锁的设计

由于增加了调度器线程,因此除了电梯和调度器共享的请求队列processQueue,还增加了调度器与输入线程的共享的请求队列waitQueue。但同步块和锁的设计无太大改变,仍然是共享对象方法加锁和线程中加锁同步块的方式。

调度器设计

本次作业中,我增加了调度器线程,其作用是管理相同类的一组电梯(也就是15个调度器)。

  1. 接受增加电梯请求,在队列里增加一个电梯进行管理并启动该电梯线程。为此,需要有与输入线程的共享对象waitQueue以及elevators.

  2. 接受来自输入线程的乘客请求,按照按序依次分配给各个电梯。为此需要有index来标示下一个分配电梯的序号,每分配完一个请求后更新index到下一个电梯。

3. 第七次作业

UML类图

 

设计思路

第七次增加了跨楼座和层之间的请求,给电梯增加了可修改的属性,以及停靠限制。

对于电梯可修改属性和停靠限制,处理很简单,只需要给电梯增添对应的属性即可。

对于跨楼座和层之间的请求。首先我采用了在输入线程静态分解的方式,具体分解方式参考停靠限制、纵向距离最短原则以及各层的空闲程度。其次在各输入线程增加了一个unreadyQueue管理还没有被触发的分请求。当前分请求完成后,在电梯线程将对应调度器线程的下一个分请求调入就绪的队列,等待分配。通过这两点完成了跨楼座和层请求的需求。

同时我设计了AllDispatcher类用于管理所有的dispatcher。它并不是一个线程。

同步块与锁的设计

在第七次作业中,由于在调度器线程中增加了一个新的unreadyQueue队列,就需要在该线程中同时维护两个共享对象,这是本次作业的锁设计的难点。在我的设计中,主要体现在要wait()时应该使用哪个对象的锁?

// Dispatcher.java
synchronized (waitQueue) {
   if (waitQueue.isEmpty()) {
       if (!waitQueue.isEnd() || !unreadyQueue.isEmpty()) {
           try {
               waitQueue.wait();
          } catch (InterruptedException e) {
               e.printStackTrace();
          }
      }
  }
   if (!waitQueue.isEmpty()) {
       request = waitQueue.getOneRequest();
  }
}

这里我使用了waitQueue的锁进行wait().

其他地方与前两次作业没有太大差异,此处不再赘述。

调度器设计

在本次作业中,由于电梯的属性可设计,因此不宜采用均分分配乘客的策略。这里我给每个电梯设计一个量busy。计算方法是busy = (1.5 * 电梯待处理请求数量 + 电梯内部请求数量) * 移动一楼层所需时间 / 容量。系数采用1.5是考虑到,平均来说,待处理请求要花费时间较内部请求长。调度器分配时查看其管理的电梯哪个busy最小,就将乘客分配给哪个电梯。

同时需求静态分解也有一定的策略可循,在满足停靠限制、纵向距离最短原则基础上,寻找尽可能空闲的层,分配给该层的调度器。这里我设计了多种计量方式,经过实验比较最终选择了一种较为简单的策略:即选择待处理请求较少的层进行横向运输。

可扩展性分析

我认为我的架构具备一定的可扩展性,比如说对于纵向的停靠限制电梯,只需要在输入线程的静态分解方法判定即可。不需要对分配策略以及代码架构进行大变动即可维持较高性能。

4. UML协作图

 

二、bug、test及hack

bug

第五次作业出现了严重bug,bug是电梯判断人能进入时应该满足内部的人数 < 电梯容量,我写成了 内部人数 <= 电梯容量。同时在look策略的一处地方也是这个人数问题。这两处bug导致了电梯运行故障,于是就wa了很多点。修复这两处bug只需要将等号去掉即可。

我痛定思痛,认为根本原因还是电梯运行逻辑太过混乱,因此也决定第六次作业进行重构。

第六次和第七次作业强测均没有出现bug。第七次作业中测出现了一些bug,主要是因为第七次作业我给调度器线程新增了一个维护的队列,刚开始并没有处理好两个队列的管理关系,导致了诸多线程问题。幸运的是,在思考过后就修改成功了。

test

由于多线程的不可复现性,因此,我的测试方法主要是随机数据进行大量测试。尽可能做到数据全覆盖且线程运行顺序多样。为此,针对第六次和第七次作业我设计了随机数据生成器。以下为部分代码:

if (period == self.period_upper):
   period = 0
   for i in range(self.elevator_add_num):
       file.write("[" + str(round(time, 1)) + "]ADD-floor-" + str(elevator_id) + "-" + str(
           self.rdfloor()) + "-" + str(self.rdcapacity()) + "-" + str(self.rdspeed()) + "-" + str(
           self.rdswiInfo()) + "\n")
       elevator_id += 1
// 周期性动态增加电梯

for i in range(self.cross_num):
   fromfloor = self.rdfloor()
   tofloor = self.rdfloor()
   frombuilding = self.rdbuilding()
   tobuilding = self.nofromrdbuilding(frombuilding)
   file.write("[" + str(round(time, 1)) + "]" + str(person_id) + "-FROM-" + frombuilding + "-" + str(fromfloor) + "-TO-" + tobuilding + "-" + str(tofloor) + "\n")
   person_id += 1
// 随机生成跨层跨楼座请求

hack

我的hack策略为针对性构造数据以及大量随机数据攻击。第五次作业时我设计了针对性数据成功hack同房人。第六次和第七次作业采取的随机数据攻击的方式进行hack,可惜的是由于多线程程序的不确定性,虽然在本地运行时hack成功了,但是提交以后bug没有得到复现。

三、心得体会

线程安全

电梯月让我初次接触了多线程编程。一个让我时刻保持警惕的问题就是线程安全,为此也是投入了一些心思。虽然三次作业并没有因为线程安全问题丢分,但是我的架构中仍存在诸多不完美的地方:

  • 没有实现较好的封装。最好的设计是将尽可能多的锁加在共享对象的方法上,以实现外部的无锁结构。离它还有很大距离。

  • 共享对象管理。在最后一次作业时新增了一个队列作为共享对象,本来以为复刻一下第一周的做法就行了,结果同时管理两个队列遇到了诸多问题。好在最后逐一解决了。

层次化设计

反思起来,层次化设计还有许多可以改进的地方。

  1. 第六、七次作业我将电梯的状态变化和策略都写在了Elevator类中,导致其异常庞大,这不是一个优秀的层次化设计,应该考虑将策略和电梯运行分离。

  2. 共享对象的设计也存在不足,我为了处理方便,在设计中多用组合关系(UML类图中可见),这存在逻辑问题。在进行更高要求的设计时很可能会进行重构。

一些感想

电梯月总算是过完了。第一周时写作业感觉挺顺的,也就没怎么测试,结果强测就来了个下马威。之后每一周都抱着很谨慎的态度对待作业,生怕强测再wa一片。

总之还是不能太浪,对待再简单的作业也得认真。

 
posted @ 2022-04-29 09:58  SleepEarlyGuy  阅读(59)  评论(2编辑  收藏  举报